From e5ea45c0bcaba7082d510b82e46ba348013d23d9 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 25 Aug 2026 09:06:09 +0100
Subject: [PATCH 1/3] fix(mocks): omit inherited explicit constraints
Closes #6670
---
.../Builders/MockImplBuilder.cs | 2 +-
.../Issue6670Tests.cs | 42 +++++++++++++++++++
2 files changed, 43 insertions(+), 1 deletion(-)
create mode 100644 tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs
diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
index 723f1a08b6..6899c932c3 100644
--- a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
@@ -588,7 +588,7 @@ private static void GenerateInterfaceMethod(CodeWriter writer, MockMemberModel m
var signatureReturnType = (method.IsVoid && !method.IsAsync) ? "void" : method.ReturnType;
var paramList = GetParameterList(method);
var typeParams = GetTypeParameterList(method);
- var constraints = GetConstraintClauses(method);
+ var constraints = GetConstraintClauses(method, forExplicitImplementation: method.ExplicitInterfaceName is not null);
writer.AppendLineIfNotEmpty(method.ObsoleteAttribute);
diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs
new file mode 100644
index 0000000000..f6e32e378f
--- /dev/null
+++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs
@@ -0,0 +1,42 @@
+namespace TUnit.Mocks.SourceGenerator.Tests;
+
+///
+/// Regression: https://github.com/thomhurst/TUnit/issues/6670
+/// A derived interface can hide a generic base method with a different return type. The base
+/// slot then needs an explicit implementation, which cannot repeat inherited constraints such
+/// as notnull (CS0460).
+///
+public class Issue6670Tests : SnapshotTestBase
+{
+ [Test]
+ public async Task Hidden_Generic_Interface_Method_Does_Not_Repeat_Constraints_On_Explicit_Implementation()
+ {
+ var source = """
+ using System.Collections.Generic;
+ using TUnit.Mocks;
+
+ public interface ITest : ITestParent
+ {
+ new IList Get() where T : notnull;
+ }
+
+ public interface ITestParent
+ {
+ IEnumerable Get() where T : notnull;
+ }
+
+ public class TestUsage
+ {
+ void M()
+ {
+ var mock = ITest.Mock();
+ }
+ }
+ """;
+
+ var errors = GetGeneratedCompilationErrors(source);
+ var constraintErrors = errors.Where(error => error.Id == "CS0460").ToList();
+
+ await Assert.That(constraintErrors).IsEmpty();
+ }
+}
From 7b9242639b547cb4f16559975be745ee399a5901 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Tue, 25 Aug 2026 09:38:14 +0100
Subject: [PATCH 2/3] fix(mocks): forward explicit generic calls
Add TUnit.Engine.Tests coverage for reflection and NativeAOT execution modes.
---
.../Builders/MockImplBuilder.cs | 2 +-
tests/TUnit.Engine.Tests/Issue6670Tests.cs | 21 +++
.../Issue6670Tests.cs | 54 +++---
...ce_Method_Generation_Snapshot.verified.txt | 172 ++++++++++++++++++
.../Bugs/6670/Issue6670MockTests.cs | 32 ++++
.../TUnit.TestProject.csproj | 2 +
6 files changed, 259 insertions(+), 24 deletions(-)
create mode 100644 tests/TUnit.Engine.Tests/Issue6670Tests.cs
create mode 100644 tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt
create mode 100644 tests/TUnit.TestProject/Bugs/6670/Issue6670MockTests.cs
diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
index 6899c932c3..594a9b283a 100644
--- a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
@@ -599,7 +599,7 @@ private static void GenerateInterfaceMethod(CodeWriter writer, MockMemberModel m
// Return type is compatible (e.g. IEnumerable.GetEnumerator → IEnumerable.GetEnumerator)
// — delegate to the public method.
var argPassList = GetArgPassList(method);
- writer.AppendLine($"{signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints} => {EscapeIdentifier(method.Name)}({argPassList});");
+ writer.AppendLine($"{signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints} => {EscapeIdentifier(method.Name)}{typeParams}({argPassList});");
}
else
{
diff --git a/tests/TUnit.Engine.Tests/Issue6670Tests.cs b/tests/TUnit.Engine.Tests/Issue6670Tests.cs
new file mode 100644
index 0000000000..850c61c637
--- /dev/null
+++ b/tests/TUnit.Engine.Tests/Issue6670Tests.cs
@@ -0,0 +1,21 @@
+using Shouldly;
+using TUnit.Engine.Tests.Enums;
+
+namespace TUnit.Engine.Tests;
+
+public class Issue6670Tests(TestMode testMode) : InvokableTestBase(testMode)
+{
+ [Test]
+ public async Task Hidden_Generic_Interface_Method_Mock_Works()
+ {
+ await RunTestsWithFilter(
+ "/*/TUnit.TestProject.Bugs._6670/Issue6670MockTests/*",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(1),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(1),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0),
+ result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
+ ]);
+ }
+}
diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs
index f6e32e378f..50209b1337 100644
--- a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs
+++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6670Tests.cs
@@ -8,35 +8,43 @@ namespace TUnit.Mocks.SourceGenerator.Tests;
///
public class Issue6670Tests : SnapshotTestBase
{
- [Test]
- public async Task Hidden_Generic_Interface_Method_Does_Not_Repeat_Constraints_On_Explicit_Implementation()
- {
- var source = """
- using System.Collections.Generic;
- using TUnit.Mocks;
+ private const string Source = """
+ using System.Collections.Generic;
+ using TUnit.Mocks;
- public interface ITest : ITestParent
- {
- new IList Get() where T : notnull;
- }
+ public interface ITest : ITestParent
+ {
+ new IList Get() where T : notnull;
+ }
- public interface ITestParent
- {
- IEnumerable Get() where T : notnull;
- }
+ public interface ITestParent
+ {
+ IEnumerable Get() where T : notnull;
+ }
- public class TestUsage
+ public class TestUsage
+ {
+ void M()
{
- void M()
- {
- var mock = ITest.Mock();
- }
+ var mock = ITest.Mock();
}
- """;
+ }
+ """;
- var errors = GetGeneratedCompilationErrors(source);
- var constraintErrors = errors.Where(error => error.Id == "CS0460").ToList();
+ [Test]
+ public async Task Hidden_Generic_Interface_Method_Does_Not_Repeat_Constraints_On_Explicit_Implementation()
+ {
+ var errors = GetGeneratedCompilationErrors(Source);
+ var genericImplementationErrors = errors
+ .Where(error => error.Id is "CS0411" or "CS0460")
+ .ToList();
- await Assert.That(constraintErrors).IsEmpty();
+ await Assert.That(genericImplementationErrors).IsEmpty();
+ }
+
+ [Test]
+ public Task Hidden_Generic_Interface_Method_Generation_Snapshot()
+ {
+ return VerifyGeneratorOutput(Source);
}
}
diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt
new file mode 100644
index 0000000000..1ba4548b22
--- /dev/null
+++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Hidden_Generic_Interface_Method_Generation_Snapshot.verified.txt
@@ -0,0 +1,172 @@
+//
+#pragma warning disable
+#nullable enable
+
+public sealed class ITestMock : global::TUnit.Mocks.Mock, global::ITest
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal ITestMock(global::ITest mockObject, global::TUnit.Mocks.MockEngine engine)
+ : base(mockObject, engine) { }
+
+ global::System.Collections.Generic.IList global::ITest.Get() => Object.Get();
+
+ global::System.Collections.Generic.IEnumerable global::ITestParent.Get() => ((global::ITestParent)Object).Get();
+}
+
+
+// ===== FILE SEPARATOR =====
+
+//
+#pragma warning disable
+#nullable enable
+
+file sealed class ITestMockImpl : global::ITest, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
+{
+ private readonly global::TUnit.Mocks.MockEngine _engine;
+
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }
+
+ internal ITestMockImpl(global::TUnit.Mocks.MockEngine engine)
+ {
+ _engine = engine;
+ }
+
+ public global::System.Collections.Generic.IList Get() where T : notnull
+ {
+ return _engine.HandleCallWithReturn>(0, "Get", global::System.Array.Empty