Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
refactor(mocks): unify Mock.Of<T>() and Mock.OfPartial<T>() into sing…
…le API

The source generator already knows whether T is an interface or a class,
so users shouldn't need to choose between Of and OfPartial. This follows
the convention of other mocking frameworks (Moq, NSubstitute) where a
single factory method handles both cases.

- Merge _factories and _partialFactories into a single unified registry
- Remove Mock.OfPartial<T>() and MockRepository.OfPartial<T>()
- Add Mock.Of<T>(params object[] constructorArgs) overloads for classes
- Derive IsPartialMock from TypeKind.Class in source generator
- Fix [GenerateMock(typeof(ConcreteClass))] to generate partial mock code
- Update analyzers, tests, snapshots, and documentation
  • Loading branch information
thomhurst committed Mar 30, 2026
commit d44ed9b65c29074611c6269c5b73cf29a4e74472
8 changes: 4 additions & 4 deletions TUnit.Mocks.Analyzers.Tests/SealedClassMockAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public static class Mock
{
public static object Of<T>() => default!;
public static object Of<T>(int behavior) => default!;
public static object OfPartial<T>(params object[] args) => default!;
public static object OfPartial<T>(int behavior, params object[] args) => default!;
public static object Of<T>(int behavior, params object[] args) => default!;
public static object Of<T>(params object[] args) => default!;
}
}
""";
Expand Down Expand Up @@ -141,7 +141,7 @@ public void Test()
}

[Test]
public async Task Sealed_Class_Via_OfPartial_Reports_TM001()
public async Task Sealed_Class_With_Constructor_Args_Reports_TM001()
{
await Verifier.VerifyAnalyzerAsync(
MockStub + """
Expand All @@ -152,7 +152,7 @@ public class TestClass
{
public void Test()
{
{|#0:TUnit.Mocks.Mock.OfPartial<MyService>()|};
{|#0:TUnit.Mocks.Mock.Of<MyService>(0, "arg")|};
}
}
""",
Expand Down
8 changes: 4 additions & 4 deletions TUnit.Mocks.Analyzers.Tests/StructMockAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public static class Mock
{
public static object Of<T>() => default!;
public static object Of<T>(int behavior) => default!;
public static object OfPartial<T>(params object[] args) => default!;
public static object OfPartial<T>(int behavior, params object[] args) => default!;
public static object Of<T>(int behavior, params object[] args) => default!;
public static object Of<T>(params object[] args) => default!;
}
}
""";
Expand Down Expand Up @@ -144,7 +144,7 @@ public void Test()
}

[Test]
public async Task Struct_Via_OfPartial_Reports_TM002()
public async Task Struct_With_Constructor_Args_Reports_TM002()
{
await Verifier.VerifyAnalyzerAsync(
MockStub + """
Expand All @@ -155,7 +155,7 @@ public class TestClass
{
public void Test()
{
{|#0:TUnit.Mocks.Mock.OfPartial<MyStruct>()|};
{|#0:TUnit.Mocks.Mock.Of<MyStruct>(0, "arg")|};
}
}
""",
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Mocks.Analyzers/SealedClassMockAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)

private static bool IsMockOfMethod(IMethodSymbol method)
{
return method.Name is "Of" or "OfPartial"
return method.Name is "Of"
&& method.ContainingType is { Name: "Mock" or "MockRepository", ContainingNamespace: { Name: "Mocks", ContainingNamespace: { Name: "TUnit", ContainingNamespace.IsGlobalNamespace: true } } }
&& method.IsGenericMethod;
}
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Mocks.Analyzers/StructMockAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)

private static bool IsMockOfMethod(IMethodSymbol method)
{
return method.Name is "Of" or "OfPartial"
return method.Name is "Of"
&& method.ContainingType is { Name: "Mock" or "MockRepository", ContainingNamespace: { Name: "Mocks", ContainingNamespace: { Name: "TUnit", ContainingNamespace.IsGlobalNamespace: true } } }
&& method.IsGenericMethod;
}
Expand Down
6 changes: 3 additions & 3 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ public class TestUsage
{
void M()
{
var mock = Mock.OfPartial<BaseService>();
var mock = Mock.Of<BaseService>();
}
}
""";
Expand Down Expand Up @@ -505,7 +505,7 @@ public class TestUsage
{
void M()
{
var mock = Mock.OfPartial<ExternalClient>();
var mock = Mock.Of<ExternalClient>();
}
}
""";
Expand Down Expand Up @@ -543,7 +543,7 @@ public class TestUsage
{
void M()
{
var mock = Mock.OfPartial<ServiceClient>();
var mock = Mock.Of<ServiceClient>();
}
}
""";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IReadWriter>(Create);
}

private static global::TUnit.Mocks.Mock<global::IReadWriter> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IReadWriter> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IReadWriter>(behavior);
var impl = new IReadWriter_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IAsyncService>(Create);
}

private static global::TUnit.Mocks.Mock<global::IAsyncService> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IAsyncService> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IAsyncService>(behavior);
var impl = new IAsyncService_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::INotifier>(Create);
}

private static global::TUnit.Mocks.Mock<global::INotifier> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::INotifier> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::INotifier>(behavior);
var impl = new INotifier_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IRepository>(Create);
}

private static global::TUnit.Mocks.Mock<global::IRepository> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IRepository> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IRepository>(behavior);
var impl = new IRepository_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::TUnit.Mocks.Generated.IMyService_Mockable>(Create);
}

private static global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IMyService_Mockable> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IMyService_Mockable> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::TUnit.Mocks.Generated.IMyService_Mockable>(behavior);
var impl = new IMyService_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::ITest>(Create);
}

private static global::TUnit.Mocks.Mock<global::ITest> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::ITest> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::ITest>(behavior);
var impl = new ITest_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IService>(Create);
}

private static global::TUnit.Mocks.Mock<global::IService> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IService> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IService>(behavior);
var impl = new IService_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IFoo>(Create);
}

private static global::TUnit.Mocks.Mock<global::IFoo> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IFoo> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IFoo>(behavior);
var impl = new IFoo_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IDictionary>(Create);
}

private static global::TUnit.Mocks.Mock<global::IDictionary> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IDictionary> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IDictionary>(behavior);
var impl = new IDictionary_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated/>
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
Expand All @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IFormatter>(Create);
}

private static global::TUnit.Mocks.Mock<global::IFormatter> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IFormatter> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IFormatter>(behavior);
var impl = new IFormatter_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IRepository>(Create);
}

private static global::TUnit.Mocks.Mock<global::IRepository> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IRepository> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IRepository>(behavior);
var impl = new IRepository_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IBufferProcessor>(Create);
}

private static global::TUnit.Mocks.Mock<global::IBufferProcessor> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IBufferProcessor> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IBufferProcessor>(behavior);
var impl = new IBufferProcessor_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::TUnit.Mocks.Generated.IServiceFactory_Mockable>(Create);
}

private static global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IServiceFactory_Mockable> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::TUnit.Mocks.Generated.IServiceFactory_Mockable> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::TUnit.Mocks.Generated.IServiceFactory_Mockable>(behavior);
var impl = new IServiceFactory_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated/>
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
Expand All @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IMyService>(Create);
}

private static global::TUnit.Mocks.Mock<global::IMyService> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IMyService> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IMyService>(behavior);
var impl = new IMyService_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated/>
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
Expand All @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::ICalculator>(Create);
}

private static global::TUnit.Mocks.Mock<global::ICalculator> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::ICalculator> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::ICalculator>(behavior);
var impl = new ICalculator_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace TUnit.Mocks.Generated
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.Mock.RegisterPartialFactory<global::ExternalLib.ExternalClient>(Create);
global::TUnit.Mocks.Mock.RegisterFactory<global::ExternalLib.ExternalClient>(Create);
}

private static global::TUnit.Mocks.Mock<global::ExternalLib.ExternalClient> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace TUnit.Mocks.Generated
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.Mock.RegisterPartialFactory<global::ExternalLib.ServiceClient>(Create);
global::TUnit.Mocks.Mock.RegisterFactory<global::ExternalLib.ServiceClient>(Create);
}

private static global::TUnit.Mocks.Mock<global::ExternalLib.ServiceClient> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace TUnit.Mocks.Generated
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::TUnit.Mocks.Mock.RegisterPartialFactory<global::BaseService>(Create);
global::TUnit.Mocks.Mock.RegisterFactory<global::BaseService>(Create);
}

private static global::TUnit.Mocks.Mock<global::BaseService> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <auto-generated/>
// <auto-generated/>
#nullable enable

namespace TUnit.Mocks.Generated
Expand All @@ -11,7 +11,7 @@ namespace TUnit.Mocks.Generated
global::TUnit.Mocks.Mock.RegisterFactory<global::IGreeter>(Create);
}

private static global::TUnit.Mocks.Mock<global::IGreeter> Create(global::TUnit.Mocks.MockBehavior behavior)
private static global::TUnit.Mocks.Mock<global::IGreeter> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)
{
var engine = new global::TUnit.Mocks.MockEngine<global::IGreeter>(behavior);
var impl = new IGreeter_MockImpl(engine);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static string Build(MockTypeModel model)
}
writer.AppendLine();

using (writer.Block($"private static global::TUnit.Mocks.Mock<{model.FullyQualifiedName}> Create(global::TUnit.Mocks.MockBehavior behavior)"))
using (writer.Block($"private static global::TUnit.Mocks.Mock<{model.FullyQualifiedName}> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)"))
{
writer.AppendLine($"var engine = new global::TUnit.Mocks.MockEngine<{model.FullyQualifiedName}>(behavior);");

Expand Down
6 changes: 3 additions & 3 deletions TUnit.Mocks.SourceGenerator/Builders/MockFactoryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static string Build(MockTypeModel model)
{
BuildWrapFactory(writer, model, safeName);
}
else if (model.IsPartialMock && !model.IsInterface)
else if (model.IsPartialMock)
{
BuildPartialFactory(writer, model, safeName);
}
Expand Down Expand Up @@ -60,7 +60,7 @@ private static void BuildInterfaceFactory(CodeWriter writer, MockTypeModel model
}
writer.AppendLine();

using (writer.Block($"private static global::TUnit.Mocks.Mock<{mockableType}> Create(global::TUnit.Mocks.MockBehavior behavior)"))
using (writer.Block($"private static global::TUnit.Mocks.Mock<{mockableType}> Create(global::TUnit.Mocks.MockBehavior behavior, object[] constructorArgs)"))
{
writer.AppendLine($"var engine = new global::TUnit.Mocks.MockEngine<{mockableType}>(behavior);");
writer.AppendLine($"var impl = new {safeName}_MockImpl(engine);");
Expand Down Expand Up @@ -101,7 +101,7 @@ private static void BuildPartialFactory(CodeWriter writer, MockTypeModel model,
writer.AppendLine("[global::System.Runtime.CompilerServices.ModuleInitializer]");
using (writer.Block("internal static void Register()"))
{
writer.AppendLine($"global::TUnit.Mocks.Mock.RegisterPartialFactory<{model.FullyQualifiedName}>(Create);");
writer.AppendLine($"global::TUnit.Mocks.Mock.RegisterFactory<{model.FullyQualifiedName}>(Create);");
}
writer.AppendLine();

Expand Down
2 changes: 1 addition & 1 deletion TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static string Build(MockTypeModel model)
{
BuildWrapMockImpl(writer, model, safeName);
}
else if (model.IsPartialMock && !model.IsInterface)
else if (model.IsPartialMock)
{
BuildPartialMockImpl(writer, model, safeName);
}
Expand Down
Loading
Loading