Skip to content

TUnit.Mocks: CS0205 "Cannot call an abstract base member" when mocking an abstract class with an abstract indexer #6516

Description

@samtrion

Description

T.Mock() fails to compile with CS0205: Cannot call an abstract base member when T is an abstract class that declares an abstract indexer (this[...]).

The generated mock implementation always emits a fallback return base[index]; (or base[index] = value; for a setter) inside the indexer override, regardless of whether the indexer is abstract. That's fine for a virtual indexer with a real base implementation, but for an abstract indexer there is no base implementation to call, so the generated code doesn't compile.

Other abstract methods on the same type (e.g. GetOrdinal(string), IsDBNull(int) on System.Data.Common.DbDataReader) mock and compile without issue, so this looks specific to how indexers are handled in the generator's fallback path.

Repro steps

repro.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="TUnit" Version="1.62.0" />
    <PackageReference Include="TUnit.Mocks" Version="1.62.0" />
  </ItemGroup>
</Project>

Repro.cs:

namespace ReproTests;

public abstract class Repository
{
    public abstract object this[int index] { get; }
    public abstract object this[string key] { get; }
}

public class MockGenTests
{
    [Test]
    public async Task Mocking_AbstractClass_WithAbstractIndexer_ShouldCompile()
    {
        var repo = Repository.Mock();
        await Task.CompletedTask;
    }
}

dotnet build fails with:

error CS0205: Cannot call an abstract base member: 'Repository.this[int]'
error CS0205: Cannot call an abstract base member: 'Repository.this[string]'

Generated code (the actual bug)

With EmitCompilerGeneratedFiles enabled, the generated RepositoryMockImpl looks like this:

file sealed class RepositoryMockImpl : global::ReproTests.Repository, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
    private readonly global::TUnit.Mocks.MockEngine<global::ReproTests.Repository> _engine;

    // ...

    public override object this[int index]
    {
        get
        {
            if (_engine.TryHandleCallWithReturn<object>(0, "get_Item", new object?[] { index }, default!, out var __result))
            {
                return __result;
            }
            return base[index]; // <-- CS0205: Repository.this[int] is abstract, there is no base member to call
        }
    }

    public override object this[string key]
    {
        get
        {
            if (_engine.TryHandleCallWithReturn<object>(1, "get_Item", new object?[] { key }, default!, out var __result))
            {
                return __result;
            }
            return base[key]; // <-- CS0205: Repository.this[string] is abstract, there is no base member to call
        }
    }

    // ...
}

For a virtual indexer, falling back to base[...] when no setup matches is reasonable. For an abstract indexer, the generator needs the same fallback strategy it already uses elsewhere for abstract methods (e.g. throw, or return default/the configured default-value-provider result) instead of unconditionally emitting a base[...] call.

Expected behavior

Repository.Mock() compiles, and calling the indexer on an unconfigured mock either:

  • returns the type's default value (consistent with how unconfigured abstract methods currently behave), or
  • throws an informative exception (e.g. in strict mode),

exactly as already happens for abstract non-indexer members.

Actual behavior

Build fails with CS0205 for every abstract indexer on the mocked type.

Real-world impact

This isn't just a contrived example — it blocks mocking System.Data.Common.DbDataReader (which declares public abstract object this[int ordinal] { get; } and public abstract object this[string name] { get; }), which in turn is blocking us from fully migrating a test suite off NSubstitute onto TUnit.Mocks. We've had to keep one test file on NSubstitute as a scoped, documented exception until this is fixed:
dailydevops/extensions.data#563

Environment

  • TUnit / TUnit.Mocks: 1.62.0 (latest at time of filing)
  • .NET SDK: 10.0.302
  • Target framework: net10.0
  • OS: Windows 10.0.26200 (win-x64)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions