Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
continue;
}

// For class partial mocks, the base class already implements (or inherits) all
// interface members — re-emitting them as `public override` fails to compile
// when the base impl is non-virtual or explicit (#5673:
// EntityEntry explicitly implements IInfrastructure<InternalEntityEntry>.Instance).
// The inherited impl satisfies the interface; the mock only needs to override
// what the class walk already collected (virtual/abstract/override members).
if (typeSymbol.TypeKind == TypeKind.Class
&& typeSymbol.FindImplementationForInterfaceMember(member) is not null)
{
continue;
}

switch (member)
{
case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary:
Expand Down Expand Up @@ -342,6 +354,13 @@ private static void ProcessClassMembers(
var key = GetMethodKey(method);
// Seed both seen sets so the interface loop doesn't re-add class members
seenFullMethods.Add(GetFullMethodKey(method));
// ref / ref readonly returns can't be routed through the mock engine —
// treat them as non-mockable so the inherited impl flows through unchanged.
if (method.ReturnsByRef || method.ReturnsByRefReadonly)
{
seenMethods.TryAdd(key, NonMockableEntry);
break;
}
if (method.IsAbstract || method.IsVirtual || method.IsOverride)
{
if (seenMethods.ContainsKey(key)) continue;
Expand Down
22 changes: 22 additions & 0 deletions TUnit.Mocks.Tests/Issue5673Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#if NET10_0_OR_GREATER
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace TUnit.Mocks.Tests;

// Regression: https://github.com/thomhurst/TUnit/issues/5673
// EntityEntry implements IInfrastructure<InternalEntityEntry> where InternalEntityEntry is
// internal to EF Core. The generated override for `Instance` referenced that internal type,
// producing CS0115 "no suitable method found to override" in external assemblies.
public class Issue5673Tests
{
[Test]
public async Task Mocking_EntityEntry_With_Internal_Return_Type_Compiles()
{
// If the generator regresses, this file fails to compile with CS0115 on 'Instance'.
// EntityEntry only exposes a (InternalEntityEntry) ctor, so pass null through the
// generated overload.
var mock = EntityEntry.Mock(internalEntry: null!, MockBehavior.Loose);
await Assert.That(mock).IsNotNull();
}
}
#endif
132 changes: 132 additions & 0 deletions TUnit.Mocks.Tests/KitchenSinkAbstractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,34 @@ public virtual void Exchange(ref int value)

// ── Generic virtual method ──
public virtual T GetDefault<T>() where T : struct => default;

// ── Abstract params method ──
public abstract int Aggregate(params int[] values);

// ── Abstract tuple return ──
public abstract (string Key, int Value) Pair(string seed);

// ── Virtual Func return ──
public virtual Func<int, int> GetAdder(int by) => x => x + by;

// ── Virtual method with nullable reference param ──
public virtual string Combine(string? head, string tail) => $"{head ?? "<null>"}/{tail}";
}

// ─── Abstract class that explicitly implements an interface member — #5673 shape ───

public interface IAbstractStateProvider
{
string GetState();
}

public abstract class AbstractWithExplicitInterfaceImpl : IAbstractStateProvider
{
// Explicit impl — not a virtual public member; mock derived class must not try
// to `override` this.
string IAbstractStateProvider.GetState() => "base-explicit";

public abstract string GetOwnName();
}

// ─── Tests ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -411,6 +439,110 @@ public async Task Generic_Virtual_Method_Unconfigured()
await Assert.That(mock.Object.GetDefault<bool>()).IsFalse();
}

// ── Abstract params method ──

[Test]
public async Task Abstract_Params_Method_Configured()
{
var mock = AbstractKitchenSink.Mock();
mock.GetName().Returns("x");
mock.Aggregate(Any()).Returns(999);

await Assert.That(mock.Object.Aggregate(1, 2)).IsEqualTo(999);
await Assert.That(mock.Object.Aggregate()).IsEqualTo(999);
mock.Aggregate(Any()).WasCalled(Times.Exactly(2));
}

// ── Abstract tuple return ──

[Test]
public async Task Abstract_Tuple_Return_Configured()
{
var mock = AbstractKitchenSink.Mock();
mock.GetName().Returns("x");
mock.Pair("seed").Returns(("k", 42));

var (key, value) = mock.Object.Pair("seed");

await Assert.That(key).IsEqualTo("k");
await Assert.That(value).IsEqualTo(42);
mock.Pair("seed").WasCalled(Times.Once);
mock.Pair("other").WasNeverCalled();
}

// ── Virtual Func return ──

[Test]
public async Task Virtual_Func_Return_Unconfigured_Uses_Base()
{
var mock = AbstractKitchenSink.Mock();
mock.GetName().Returns("x");

var fn = mock.Object.GetAdder(10);

await Assert.That(fn(5)).IsEqualTo(15);
}

[Test]
public async Task Virtual_Func_Return_Configured_And_Verified()
{
var mock = AbstractKitchenSink.Mock();
mock.GetName().Returns("x");
Func<int, int> doubler = x => x * 2;
mock.GetAdder(7).Returns(doubler);

var fn = mock.Object.GetAdder(7);

await Assert.That(fn(5)).IsEqualTo(10);
mock.GetAdder(7).WasCalled(Times.Once);
mock.GetAdder(0).WasNeverCalled();
}

// ── Virtual nullable reference param ──

[Test]
public async Task Virtual_Nullable_Reference_Param_Null_Flows_To_Base()
{
var mock = AbstractKitchenSink.Mock();
mock.GetName().Returns("x");

await Assert.That(mock.Object.Combine(null, "y")).IsEqualTo("<null>/y");
}

[Test]
public async Task Virtual_Nullable_Reference_Param_Configured_And_Verified()
{
var mock = AbstractKitchenSink.Mock();
mock.GetName().Returns("x");
mock.Combine(IsNull<string>(), Any()).Returns("null-path");
mock.Combine("p-", Any()).Returns("prefixed");

await Assert.That(mock.Object.Combine(null, "z")).IsEqualTo("null-path");
await Assert.That(mock.Object.Combine("p-", "z")).IsEqualTo("prefixed");

mock.Combine(IsNull<string>(), Any()).WasCalled(Times.Once);
mock.Combine("p-", Any()).WasCalled(Times.Once);
}

// ── Abstract class with explicit interface impl (#5673 shape) ──

[Test]
public async Task Abstract_With_Explicit_Interface_Impl_Inherits_Explicit_Body()
{
var mock = AbstractWithExplicitInterfaceImpl.Mock();
mock.GetOwnName().Returns("own");

// Abstract member of the class is mockable AND verifiable.
await Assert.That(mock.Object.GetOwnName()).IsEqualTo("own");
await Assert.That(mock.Object.GetOwnName()).IsEqualTo("own");
mock.GetOwnName().WasCalled(Times.Exactly(2));

// Interface member was explicitly implemented on the abstract base — derived
// mock inherits that impl (generator must not emit `public override` for it).
IAbstractStateProvider asProvider = mock.Object;
await Assert.That(asProvider.GetState()).IsEqualTo("base-explicit");
}

// ── Verification on abstract and virtual methods ──

[Test]
Expand Down
155 changes: 155 additions & 0 deletions TUnit.Mocks.Tests/KitchenSinkConcreteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,55 @@ public virtual void Modify(ref int value)

// ── Virtual property (own) ──
public virtual string Description { get; set; } = "default-desc";

// ── Virtual params array ──
public virtual int Total(params int[] values)
{
int sum = 0;
foreach (var v in values) sum += v;
return sum;
}

// ── Virtual tuple return ──
public virtual (int Count, string Label) Describe() => (0, "base");

// ── Virtual delegate return ──
public virtual Func<int, int> GetMultiplier(int factor) => x => x * factor;

// ── Virtual method with nullable reference param ──
public virtual string Combine(string? prefix, string value) => $"{prefix ?? ""}{value}";
}

// ─── Interface with an internally-typed return and a concrete class that
// implements it explicitly — regression shape for #5673
// ─────────────────────────────────────────────────────────────────────────────

public interface IHasHiddenInstance
{
IHiddenState Instance { get; }
}

public interface IHiddenState
{
string Marker { get; }
}

public sealed class HiddenState : IHiddenState
{
public string Marker { get; init; } = "";
}

public class HasExplicitInstance : IHasHiddenInstance
{
private readonly HiddenState _state;

public HasExplicitInstance() : this(new HiddenState { Marker = "default" }) { }
public HasExplicitInstance(HiddenState state) { _state = state; }

// Explicit interface impl — *not* a virtual public member on the class.
IHiddenState IHasHiddenInstance.Instance => _state;

public virtual string Describe() => $"explicit:{_state.Marker}";
}

// ─── Tests ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -360,4 +409,110 @@ public async Task Base_Virtual_Event_Can_Be_Raised()

await Assert.That(received).IsEqualTo(42);
}

// ── Params virtual ──

[Test]
public async Task Virtual_Params_Unconfigured_Uses_Base()
{
var mock = ConcreteKitchenSink.Mock();

await Assert.That(mock.Object.Total(1, 2, 3)).IsEqualTo(6);
}

[Test]
public async Task Virtual_Params_Configured()
{
var mock = ConcreteKitchenSink.Mock();
mock.Total(Any()).Returns(1000);

await Assert.That(mock.Object.Total(1, 2, 3)).IsEqualTo(1000);
await Assert.That(mock.Object.Total()).IsEqualTo(1000);
mock.Total(Any()).WasCalled(Times.Exactly(2));
}

// ── Virtual tuple return ──

[Test]
public async Task Virtual_Tuple_Return_Configured()
{
var mock = ConcreteKitchenSink.Mock();
mock.Describe().Returns((5, "mocked"));

var (count, label) = mock.Object.Describe();

await Assert.That(count).IsEqualTo(5);
await Assert.That(label).IsEqualTo("mocked");
mock.Describe().WasCalled(Times.Once);
}

// ── Virtual delegate return ──

[Test]
public async Task Virtual_Delegate_Return_Unconfigured_Uses_Base()
{
var mock = ConcreteKitchenSink.Mock();

var fn = mock.Object.GetMultiplier(3);

await Assert.That(fn(4)).IsEqualTo(12);
}

[Test]
public async Task Virtual_Delegate_Return_Configured()
{
var mock = ConcreteKitchenSink.Mock();
Func<int, int> tripler = x => x * 3;
mock.GetMultiplier(5).Returns(tripler);

var fn = mock.Object.GetMultiplier(5);

await Assert.That(fn(4)).IsEqualTo(12);
mock.GetMultiplier(5).WasCalled(Times.Once);
mock.GetMultiplier(9).WasNeverCalled();
}

// ── Nullable reference param ──

[Test]
public async Task Virtual_Nullable_Reference_Param_Unconfigured_Uses_Base()
{
var mock = ConcreteKitchenSink.Mock();

await Assert.That(mock.Object.Combine(null, "x")).IsEqualTo("x");
await Assert.That(mock.Object.Combine("pre-", "x")).IsEqualTo("pre-x");
}

[Test]
public async Task Virtual_Nullable_Reference_Param_Configured_And_Verified()
{
var mock = ConcreteKitchenSink.Mock();
mock.Combine(IsNull<string>(), Any()).Returns("null-path");
mock.Combine("p-", Any()).Returns("prefixed");

await Assert.That(mock.Object.Combine(null, "x")).IsEqualTo("null-path");
await Assert.That(mock.Object.Combine("p-", "x")).IsEqualTo("prefixed");

mock.Combine(IsNull<string>(), Any()).WasCalled(Times.Once);
mock.Combine("p-", Any()).WasCalled(Times.Once);
}

// ── Class with explicit interface impl of inaccessible-shaped member (#5673) ──

[Test]
public async Task Class_With_Explicit_Interface_Impl_Compiles_And_Inherits_Impl()
{
// Mock should compile despite the base explicitly implementing IHasHiddenInstance.Instance.
// Accessing through the interface flows to the base's explicit impl.
var mock = HasExplicitInstance.Mock();

IHasHiddenInstance asInterface = mock.Object;
await Assert.That(asInterface.Instance.Marker).IsEqualTo("default");

// Own virtual on the class is still mockable AND verifiable.
mock.Describe().Returns("mocked");
await Assert.That(mock.Object.Describe()).IsEqualTo("mocked");
await Assert.That(mock.Object.Describe()).IsEqualTo("mocked");
mock.Describe().WasCalled(Times.Exactly(2));
}
}
Loading
Loading