Skip to content

[Bug] TUnit.Mocks: cannot mock IEquatable<T>.Equals — setup extension shadowed by object.Equals #5675

@thomhurst

Description

@thomhurst

Description

Mocking a class that implements IEquatable<T> (with T = the mocked type itself) produces unusable setup: mock.Equals(other).Returns(...) fails to compile because the compiler resolves mock.Equals(...) to object.Equals(object?) (inherited by Mock<T>) instead of to the generator-emitted setup extension method.

Minimal repro

```csharp
public class SelfEquatable : IEquatable
{
public virtual bool Equals(SelfEquatable? other) => ReferenceEquals(this, other);
public override int GetHashCode() => 0;
}

var mock = SelfEquatable.Mock();
var other = new SelfEquatable();
mock.Equals(other).Returns(true); // CS1061 — bool has no Returns
```

Error:
```
error CS1061: 'bool' does not contain a definition for 'Returns' ...
```

Expected

mock.Equals(other).Returns(...) should bind to the generated setup extension and let the user configure and verify the virtual Equals implementation just like any other virtual method.

Actual

mock.Equals(other) binds to the instance method bool object.Equals(object?), which returns bool. The setup extension is never reached, so Equals can't be configured or verified.

Suggested fix

Two options for the generator:

  1. Emit a disambiguating helper name for members that collide with object's virtual surface (Equals, GetHashCode, ToString) — e.g. mock.EqualsOf(...).
  2. Generate an instance method on Mock<T> (not an extension) with a signature more specific than object.Equals(object?), taking Arg<SelfEquatable?> so overload resolution picks it.

Instructions for the fixing agent

Work test-first. Before touching the generator:

  1. Add a dedicated test that reproduces this failure. The entry point already exists: unskip and restore the T8 block in TUnit.Mocks.Tests/KitchenSinkEdgeCasesTests.cs (both the SelfEquatable type and the T8_Self_Referential_IEquatable_Mockable test) and confirm it fails with the exact error above in the current codebase.
  2. Implement the fix in the generator (see Suggested fix).
  3. Re-run the test and confirm it passes. The test must cover:
    • Returns(...) actually intercepts the Equals call (setup works).
    • WasCalled/WasNeverCalled tracks the call (verification works).
    • Both the direct mock.Object.Equals(other) path and the IEquatable<T>.Equals interface-cast path route to the same setup.
  4. Run the full TUnit.Mocks.Tests and TUnit.Mocks.SourceGenerator.Tests suites to confirm no regression.
  5. The test must stay in the suite so any future regression is caught in CI.

Context

Found while adding KitchenSink edge-case coverage in #5674. Tracked in KitchenSinkEdgeCasesTests.cs as T8 SKIPPED.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions