From 3666a29aab4560881ebc427d25f0427d93ca122b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:15:21 +0100 Subject: [PATCH] fix: generate valid mock class names for generic interfaces with non-built-in type arguments (#5403) The mock source generator produced invalid class names containing `global::` when generic type arguments were non-built-in types (enums, classes, etc.). For example, `IFoo` generated `IFoo_global::Sandbox_SomeEnum_Mock` which fails to compile. Rewrote `SanitizeIdentifier` to use a robust character-based approach that strips all `global::` prefixes and replaces any non-identifier character with `_`. Also strips same-namespace qualifications from generic type arguments for cleaner names (`IFoo_SomeEnum_Mock` instead of `IFoo_Sandbox_SomeEnum_Mock`). Added 5 snapshot tests and 8 functional tests covering enum, class, nested namespace, multiple non-built-in, and nested generic type arguments. --- .../MockGeneratorTests.cs | 168 ++++++++++++++ ...face_With_Class_Type_Argument.verified.txt | 205 ++++++++++++++++++ ...rface_With_Enum_Type_Argument.verified.txt | 112 ++++++++++ ...le_Non_Builtin_Type_Arguments.verified.txt | 199 +++++++++++++++++ ..._Nested_Generic_Type_Argument.verified.txt | 112 ++++++++++ ...ested_Namespace_Type_Argument.verified.txt | 205 ++++++++++++++++++ .../Builders/MockImplBuilder.cs | 67 ++++-- .../GenericInterfaceTypeArgumentTests.cs | 143 ++++++++++++ 8 files changed, 1187 insertions(+), 24 deletions(-) create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Enum_Type_Argument.verified.txt create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Generic_Type_Argument.verified.txt create mode 100644 TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt create mode 100644 TUnit.Mocks.Tests/GenericInterfaceTypeArgumentTests.cs diff --git a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index a45c90a025..12bc717b98 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs +++ b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs @@ -678,6 +678,174 @@ void M() return VerifyGeneratorOutput(source); } + [Test] + public Task Generic_Interface_With_Enum_Type_Argument() + { + var source = """ + using TUnit.Mocks; + + namespace Sandbox + { + public enum SomeEnum + { + Value1, + Value2 + } + + public interface IFoo + { + T Value { get; } + } + + public class TestUsage + { + void M() + { + var mock = IFoo.Mock(); + } + } + } + """; + + return VerifyGeneratorOutput(source); + } + + [Test] + public Task Generic_Interface_With_Class_Type_Argument() + { + var source = """ + using TUnit.Mocks; + + namespace Sandbox + { + public class Bar + { + public string Name { get; set; } = ""; + } + + public interface IFoo + { + T Value { get; } + void Process(T item); + } + + public class TestUsage + { + void M() + { + var mock = IFoo.Mock(); + } + } + } + """; + + return VerifyGeneratorOutput(source); + } + + [Test] + public Task Generic_Interface_With_Nested_Namespace_Type_Argument() + { + var source = """ + using TUnit.Mocks; + + namespace Outer.Inner + { + public class Config + { + public int Timeout { get; set; } + } + } + + namespace Sandbox + { + public interface IService + { + T GetConfig(); + void Apply(T config); + } + + public class TestUsage + { + void M() + { + var mock = IService.Mock(); + } + } + } + """; + + return VerifyGeneratorOutput(source); + } + + [Test] + public Task Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments() + { + var source = """ + using TUnit.Mocks; + + namespace Sandbox + { + public class Entity + { + public int Id { get; set; } + } + + public enum Status + { + Active, + Inactive + } + + public interface IMapper + { + TOut Map(TIn input); + } + + public class TestUsage + { + void M() + { + var mock = IMapper.Mock(); + } + } + } + """; + + return VerifyGeneratorOutput(source); + } + + [Test] + public Task Generic_Interface_With_Nested_Generic_Type_Argument() + { + var source = """ + using System.Collections.Generic; + using TUnit.Mocks; + + namespace Sandbox + { + public class Item + { + public string Name { get; set; } = ""; + } + + public interface IProvider + { + T Get(); + } + + public class TestUsage + { + void M() + { + var mock = IProvider>.Mock(); + } + } + } + """; + + return VerifyGeneratorOutput(source); + } + [Test] public Task Interface_With_Unconstrained_Nullable_Generic() { diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt new file mode 100644 index 0000000000..e216d9570d --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Class_Type_Argument.verified.txt @@ -0,0 +1,205 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + public sealed class IFoo_Bar_Mock : global::TUnit.Mocks.Mock>, global::Sandbox.IFoo + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IFoo_Bar_Mock(global::Sandbox.IFoo mockObject, global::TUnit.Mocks.MockEngine> engine) + : base(mockObject, engine) { } + + void global::Sandbox.IFoo.Process(global::Sandbox.Bar item) + { + Object.Process(item); + } + + global::Sandbox.Bar global::Sandbox.IFoo.Value { get => Object.Value; } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + file sealed class IFoo_Bar_MockImpl : global::Sandbox.IFoo, 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 IFoo_Bar_MockImpl(global::TUnit.Mocks.MockEngine> engine) + { + _engine = engine; + } + + public void Process(global::Sandbox.Bar item) + { + _engine.HandleCall(1, "Process", item); + } + + public global::Sandbox.Bar Value + { + get => _engine.HandleCallWithReturn(0, "get_Value", global::System.Array.Empty(), default!); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class IFoo_Bar_MockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory>(Create); + } + + internal static global::TUnit.Mocks.Mock> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::Sandbox.IFoo' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine>(behavior); + var impl = new IFoo_Bar_MockImpl(engine); + engine.Raisable = impl; + var mock = new IFoo_Bar_Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class Sandbox_IFoo_Sandbox_Bar__MockMemberExtensions + { + public static Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Process(this global::TUnit.Mocks.Mock> mock, global::TUnit.Mocks.Arguments.Arg item) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { item.Matcher }; + return new Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Process", matchers); + } + + public static Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Process(this global::TUnit.Mocks.Mock> mock, global::System.Func item) + { + global::TUnit.Mocks.Arguments.Arg __fa_item = item; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_item.Matcher }; + return new Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Process", matchers); + } + + extension(global::TUnit.Mocks.Mock> mock) + { + public global::TUnit.Mocks.PropertyMockCall Value + => new(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, 0, "Value", true, false); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; + + internal Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + _ = EnsureSetup(); + } + + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; + + /// + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Returns() { EnsureSetup().Returns(); return this; } + /// + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Execute a typed callback using the actual method parameters. + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(args => callback((global::Sandbox.Bar)args[0]!)); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public Sandbox_IFoo_Sandbox_Bar__Process_M1_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((global::Sandbox.Bar)args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class Sandbox_IFoo_Sandbox_Bar__MockStaticExtension + { + extension(global::Sandbox.IFoo) + { + public static global::TUnit.Mocks.Generated.Sandbox.IFoo_Bar_Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return (global::TUnit.Mocks.Generated.Sandbox.IFoo_Bar_Mock)global::TUnit.Mocks.Mock.Of>(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Enum_Type_Argument.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Enum_Type_Argument.verified.txt new file mode 100644 index 0000000000..cd5942f7de --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Enum_Type_Argument.verified.txt @@ -0,0 +1,112 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + public sealed class IFoo_SomeEnum_Mock : global::TUnit.Mocks.Mock>, global::Sandbox.IFoo + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IFoo_SomeEnum_Mock(global::Sandbox.IFoo mockObject, global::TUnit.Mocks.MockEngine> engine) + : base(mockObject, engine) { } + + global::Sandbox.SomeEnum global::Sandbox.IFoo.Value { get => Object.Value; } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + file sealed class IFoo_SomeEnum_MockImpl : global::Sandbox.IFoo, 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 IFoo_SomeEnum_MockImpl(global::TUnit.Mocks.MockEngine> engine) + { + _engine = engine; + } + + public global::Sandbox.SomeEnum Value + { + get => _engine.HandleCallWithReturn(0, "get_Value", global::System.Array.Empty(), default); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class IFoo_SomeEnum_MockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory>(Create); + } + + internal static global::TUnit.Mocks.Mock> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::Sandbox.IFoo' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine>(behavior); + var impl = new IFoo_SomeEnum_MockImpl(engine); + engine.Raisable = impl; + var mock = new IFoo_SomeEnum_Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class Sandbox_IFoo_Sandbox_SomeEnum__MockMemberExtensions + { + extension(global::TUnit.Mocks.Mock> mock) + { + public global::TUnit.Mocks.PropertyMockCall Value + => new(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, 0, "Value", true, false); + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class Sandbox_IFoo_Sandbox_SomeEnum__MockStaticExtension + { + extension(global::Sandbox.IFoo) + { + public static global::TUnit.Mocks.Generated.Sandbox.IFoo_SomeEnum_Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return (global::TUnit.Mocks.Generated.Sandbox.IFoo_SomeEnum_Mock)global::TUnit.Mocks.Mock.Of>(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt new file mode 100644 index 0000000000..2766fa1f4d --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Multiple_Non_Builtin_Type_Arguments.verified.txt @@ -0,0 +1,199 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + public sealed class IMapper_Entity_Status_Mock : global::TUnit.Mocks.Mock>, global::Sandbox.IMapper + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IMapper_Entity_Status_Mock(global::Sandbox.IMapper mockObject, global::TUnit.Mocks.MockEngine> engine) + : base(mockObject, engine) { } + + global::Sandbox.Status global::Sandbox.IMapper.Map(global::Sandbox.Entity input) => Object.Map(input); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + file sealed class IMapper_Entity_Status_MockImpl : global::Sandbox.IMapper, 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 IMapper_Entity_Status_MockImpl(global::TUnit.Mocks.MockEngine> engine) + { + _engine = engine; + } + + public global::Sandbox.Status Map(global::Sandbox.Entity input) + { + return _engine.HandleCallWithReturn(0, "Map", input, default); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class IMapper_Entity_Status_MockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory>(Create); + } + + internal static global::TUnit.Mocks.Mock> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::Sandbox.IMapper' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine>(behavior); + var impl = new IMapper_Entity_Status_MockImpl(engine); + engine.Raisable = impl; + var mock = new IMapper_Entity_Status_Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__MockMemberExtensions + { + public static Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Map(this global::TUnit.Mocks.Mock> mock, global::TUnit.Mocks.Arguments.Arg input) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { input.Matcher }; + return new Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Map", matchers); + } + + public static Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Map(this global::TUnit.Mocks.Mock> mock, global::System.Func input) + { + global::TUnit.Mocks.Arguments.Arg __fa_input = input; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_input.Matcher }; + return new Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Map", matchers); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.MethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; + + internal Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + })!; + + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Returns(global::Sandbox.Status value) { EnsureSetup().Returns(value); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall ReturnsSequentially(params global::Sandbox.Status[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((global::Sandbox.Entity)args[0]!)); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(args => callback((global::Sandbox.Entity)args[0]!)); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__Map_M0_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((global::Sandbox.Entity)args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class Sandbox_IMapper_Sandbox_Entity_Sandbox_Status__MockStaticExtension + { + extension(global::Sandbox.IMapper) + { + public static global::TUnit.Mocks.Generated.Sandbox.IMapper_Entity_Status_Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return (global::TUnit.Mocks.Generated.Sandbox.IMapper_Entity_Status_Mock)global::TUnit.Mocks.Mock.Of>(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Generic_Type_Argument.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Generic_Type_Argument.verified.txt new file mode 100644 index 0000000000..3900ccd423 --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Generic_Type_Argument.verified.txt @@ -0,0 +1,112 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + public sealed class IProvider_System_Collections_Generic_List_Item_Mock : global::TUnit.Mocks.Mock>>, global::Sandbox.IProvider> + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IProvider_System_Collections_Generic_List_Item_Mock(global::Sandbox.IProvider> mockObject, global::TUnit.Mocks.MockEngine>> engine) + : base(mockObject, engine) { } + + global::System.Collections.Generic.List global::Sandbox.IProvider>.Get() => Object.Get(); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + file sealed class IProvider_System_Collections_Generic_List_Item_MockImpl : global::Sandbox.IProvider>, 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 IProvider_System_Collections_Generic_List_Item_MockImpl(global::TUnit.Mocks.MockEngine>> engine) + { + _engine = engine; + } + + public global::System.Collections.Generic.List Get() + { + return _engine.HandleCallWithReturn>(0, "Get", global::System.Array.Empty(), default!); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class IProvider_System_Collections_Generic_List_Item_MockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory>>(Create); + } + + internal static global::TUnit.Mocks.Mock>> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::Sandbox.IProvider>' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine>>(behavior); + var impl = new IProvider_System_Collections_Generic_List_Item_MockImpl(engine); + engine.Raisable = impl; + var mock = new IProvider_System_Collections_Generic_List_Item_Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class Sandbox_IProvider_System_Collections_Generic_List_Sandbox_Item__MockMemberExtensions + { + public static global::TUnit.Mocks.MockMethodCall> Get(this global::TUnit.Mocks.Mock>> mock) + { + var matchers = global::System.Array.Empty(); + return new global::TUnit.Mocks.MockMethodCall>(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "Get", matchers); + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class Sandbox_IProvider_System_Collections_Generic_List_Sandbox_Item__MockStaticExtension + { + extension(global::Sandbox.IProvider>) + { + public static global::TUnit.Mocks.Generated.Sandbox.IProvider_System_Collections_Generic_List_Item_Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return (global::TUnit.Mocks.Generated.Sandbox.IProvider_System_Collections_Generic_List_Item_Mock)global::TUnit.Mocks.Mock.Of>>(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt new file mode 100644 index 0000000000..199200a7fd --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Generic_Interface_With_Nested_Namespace_Type_Argument.verified.txt @@ -0,0 +1,205 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + public sealed class IService_Outer_Inner_Config_Mock : global::TUnit.Mocks.Mock>, global::Sandbox.IService + { + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal IService_Outer_Inner_Config_Mock(global::Sandbox.IService mockObject, global::TUnit.Mocks.MockEngine> engine) + : base(mockObject, engine) { } + + global::Outer.Inner.Config global::Sandbox.IService.GetConfig() => Object.GetConfig(); + + void global::Sandbox.IService.Apply(global::Outer.Inner.Config config) + { + Object.Apply(config); + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated.Sandbox +{ + file sealed class IService_Outer_Inner_Config_MockImpl : global::Sandbox.IService, 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 IService_Outer_Inner_Config_MockImpl(global::TUnit.Mocks.MockEngine> engine) + { + _engine = engine; + } + + public global::Outer.Inner.Config GetConfig() + { + return _engine.HandleCallWithReturn(0, "GetConfig", global::System.Array.Empty(), default!); + } + + public void Apply(global::Outer.Inner.Config config) + { + _engine.HandleCall(1, "Apply", config); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } + + file static class IService_Outer_Inner_Config_MockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.MockRegistry.RegisterFactory>(Create); + } + + internal static global::TUnit.Mocks.Mock> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs) + { + if (constructorArgs.Length > 0) throw new global::System.ArgumentException($"Interface mock 'global::Sandbox.IService' does not support constructor arguments, but {constructorArgs.Length} were provided."); + var engine = new global::TUnit.Mocks.MockEngine>(behavior); + var impl = new IService_Outer_Inner_Config_MockImpl(engine); + engine.Raisable = impl; + var mock = new IService_Outer_Inner_Config_Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class Sandbox_IService_Outer_Inner_Config__MockMemberExtensions + { + public static global::TUnit.Mocks.MockMethodCall GetConfig(this global::TUnit.Mocks.Mock> mock) + { + var matchers = global::System.Array.Empty(); + return new global::TUnit.Mocks.MockMethodCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 0, "GetConfig", matchers); + } + + public static Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Apply(this global::TUnit.Mocks.Mock> mock, global::TUnit.Mocks.Arguments.Arg config) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { config.Matcher }; + return new Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Apply", matchers); + } + + public static Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Apply(this global::TUnit.Mocks.Mock> mock, global::System.Func config) + { + global::TUnit.Mocks.Arguments.Arg __fa_config = config; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_config.Matcher }; + return new Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall(global::TUnit.Mocks.MockRegistry.GetEngine(mock), 1, "Apply", matchers); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder? _builder; + private bool _builderInitialized; + private object? _builderLock; + + internal Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + _ = EnsureSetup(); + } + + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => + global::System.Threading.LazyInitializer.EnsureInitialized(ref _builder, ref _builderInitialized, ref _builderLock, () => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + })!; + + /// + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Returns() { EnsureSetup().Returns(); return this; } + /// + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Execute a typed callback using the actual method parameters. + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(args => callback((global::Outer.Inner.Config)args[0]!)); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public Sandbox_IService_Outer_Inner_Config__Apply_M1_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((global::Outer.Inner.Config)args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks +{ + public static class Sandbox_IService_Outer_Inner_Config__MockStaticExtension + { + extension(global::Sandbox.IService) + { + public static global::TUnit.Mocks.Generated.Sandbox.IService_Outer_Inner_Config_Mock Mock(global::TUnit.Mocks.MockBehavior behavior = global::TUnit.Mocks.MockBehavior.Loose) + { + return (global::TUnit.Mocks.Generated.Sandbox.IService_Outer_Inner_Config_Mock)global::TUnit.Mocks.Mock.Of>(behavior); + } + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated; \ No newline at end of file diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs index c4f6a59f22..c986136f1b 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs @@ -1291,15 +1291,7 @@ internal static string GetArgPassList(MockMemberModel method) public static string GetSafeName(string typeName) { - return typeName - .Replace("global::", "") - .Replace(".", "_") - .Replace("<", "_") - .Replace(">", "_") - .Replace(",", "_") - .Replace("[", "_") - .Replace("]", "_") - .Replace(" ", ""); + return SanitizeIdentifier(typeName); } /// @@ -1318,15 +1310,23 @@ public static string GetCompositeSafeName(MockTypeModel model) /// /// Gets a short safe name derived from just the type name (without namespace), /// sanitized for generic type arguments. Produces readable names like - /// "IGreeter" instead of "MyApp_IGreeter". + /// "IGreeter_" instead of "MyApp_IGreeter_" and "IFoo_SomeEnum_" instead of + /// "IFoo_Sandbox_SomeEnum_" when the type argument shares the outer namespace. /// public static string GetShortSafeName(MockTypeModel model) { var name = StripGlobalPrefix(model.FullyQualifiedName); + var hasNamespace = !IsGlobalNamespace(model.Namespace); - if (!IsGlobalNamespace(model.Namespace) && name.StartsWith(model.Namespace + ".")) + if (hasNamespace && name.StartsWith(model.Namespace + ".")) name = name.Substring(model.Namespace.Length + 1); + // Strip same-namespace qualifications from generic type arguments so that + // IFoo becomes IFoo (not IFoo). + // Cross-namespace args are kept for disambiguation. + if (hasNamespace) + name = name.Replace("global::" + model.Namespace + ".", ""); + return SanitizeIdentifier(name); } @@ -1373,19 +1373,38 @@ private static string StripGlobalPrefix(string name) private static string SanitizeIdentifier(string name) { - var result = name - .Replace(".", "_") - .Replace("<", "_") - .Replace(">", "_") - .Replace(",", "_") - .Replace("[", "_") - .Replace("]", "_") - .Replace(" ", ""); - - while (result.Contains("__")) - result = result.Replace("__", "_"); - - return result; + name = name.Replace("global::", ""); + + var sb = new System.Text.StringBuilder(name.Length); + var lastWasUnderscore = false; + + foreach (var c in name) + { + if (c == ' ') + continue; + + if (char.IsLetterOrDigit(c) || c == '_') + { + if (c == '_') + { + if (lastWasUnderscore) + continue; + lastWasUnderscore = true; + } + else + { + lastWasUnderscore = false; + } + sb.Append(c); + } + else if (!lastWasUnderscore) + { + sb.Append('_'); + lastWasUnderscore = true; + } + } + + return sb.ToString(); } private static bool IsGlobalNamespace(string ns) diff --git a/TUnit.Mocks.Tests/GenericInterfaceTypeArgumentTests.cs b/TUnit.Mocks.Tests/GenericInterfaceTypeArgumentTests.cs new file mode 100644 index 0000000000..b7bc054bb5 --- /dev/null +++ b/TUnit.Mocks.Tests/GenericInterfaceTypeArgumentTests.cs @@ -0,0 +1,143 @@ +using TUnit.Mocks; +using TUnit.Mocks.Arguments; + +namespace TUnit.Mocks.Tests; + +/// +/// Regression tests for https://github.com/thomhurst/TUnit/issues/5403 +/// Generic interfaces with non-built-in type arguments (enums, classes, nested namespaces, +/// multiple type arguments, nested generics) must produce valid generated class names. +/// +public class GenericInterfaceTypeArgumentTests +{ + public enum Priority + { + Low, + Medium, + High + } + + public class ItemConfig + { + public string Name { get; set; } = ""; + public int Value { get; set; } + } + + public interface IValueHolder + { + T Value { get; } + void SetValue(T value); + } + + public interface IMapper + { + TOut Map(TIn input); + } + + public interface IProvider + { + T Get(); + List GetAll(); + } + + [Test] + public async Task Generic_Interface_With_Enum_Type_Argument() + { + var mock = Mock.Of>(); + mock.Value.Returns(Priority.High); + + IValueHolder holder = mock.Object; + var result = holder.Value; + + await Assert.That(result).IsEqualTo(Priority.High); + } + + [Test] + public async Task Generic_Interface_With_Enum_Via_Static_Extension() + { + var mock = IValueHolder.Mock(); + mock.Value.Returns(Priority.Medium); + + IValueHolder holder = mock.Object; + var result = holder.Value; + + await Assert.That(result).IsEqualTo(Priority.Medium); + } + + [Test] + public async Task Generic_Interface_With_Class_Type_Argument() + { + var mock = Mock.Of>(); + var config = new ItemConfig { Name = "Test", Value = 42 }; + mock.Value.Returns(config); + + IValueHolder holder = mock.Object; + var result = holder.Value; + + await Assert.That(result).IsSameReferenceAs(config); + await Assert.That(result.Name).IsEqualTo("Test"); + await Assert.That(result.Value).IsEqualTo(42); + } + + [Test] + public async Task Generic_Interface_With_Class_Via_Static_Extension() + { + var mock = IValueHolder.Mock(); + var config = new ItemConfig { Name = "Ext", Value = 99 }; + mock.Value.Returns(config); + + IValueHolder holder = mock.Object; + var result = holder.Value; + + await Assert.That(result.Name).IsEqualTo("Ext"); + } + + [Test] + public async Task Generic_Interface_With_Two_Non_Builtin_Type_Arguments() + { + var mock = Mock.Of>(); + mock.Map(Any()).Returns(Priority.High); + + IMapper mapper = mock.Object; + var result = mapper.Map(new ItemConfig { Name = "X", Value = 1 }); + + await Assert.That(result).IsEqualTo(Priority.High); + } + + [Test] + public async Task Generic_Interface_With_Nested_Generic_Type_Argument() + { + var mock = Mock.Of>>(); + var items = new List { new() { Name = "A", Value = 1 } }; + mock.Get().Returns(items); + + IProvider> provider = mock.Object; + var result = provider.Get(); + + await Assert.That(result.Count).IsEqualTo(1); + await Assert.That(result[0].Name).IsEqualTo("A"); + } + + [Test] + public void Generic_Interface_With_Enum_Void_Method_Does_Not_Throw() + { + var mock = Mock.Of>(); + + IValueHolder holder = mock.Object; + holder.SetValue(Priority.Low); + } + + [Test] + public void Generic_Interface_With_Enum_Verify_Calls() + { + var mock = Mock.Of>(); + + IValueHolder holder = mock.Object; + holder.SetValue(Priority.High); + holder.SetValue(Priority.Low); + + mock.SetValue(Any()).WasCalled(Times.Exactly(2)); + mock.SetValue(Priority.High).WasCalled(Times.Once); + mock.SetValue(Priority.Low).WasCalled(Times.Once); + } +}