基于C# Moq自动生成MakeSut(out...)方法的实现方案
在C#中用Moq自动生成MakeSut(out ...)方法的方案
需求背景
假设待测试类有如下构造函数:
public MyService(IFooStore store, IBarService service, IModeratelyLongDependencyName dependency3) { // ... }
希望自动生成如下MakeSut方法,通过Moq创建所有依赖的模拟对象,同时暴露out参数方便后续Setup/Verify:
private static MyService MakeSut( out Mock<IFooStore> mockStore, out Mock<IBarService> mockService, out Mock<IModeratelyLongDependencyName> mockDependency3) { mockStore = new(); mockService = new(); mockDependency3 = new(); return new MyService ( mockStore.Object, mockService.Object, mockDependency3.Object ); }
使用时可以只关注需要的模拟对象,忽略其他:
// arrange MyService sut = MakeSut(out var mockStore, out _, out _); mockStore.Setup(s => s.GetFoo(It.IsAny<int>())).ReturnsAsync(new Foo(){/*...*/}); // act sut.DoTheThing(); // assert mockStore.Verify(...);
解决方案
一、基于Roslyn编写自定义代码生成工具
Resharper自定义模式无法满足需求的话,可以用Roslyn(.NET编译器平台)解析C#语法树,自动生成MakeSut方法。核心思路是:
- 解析目标类的构造函数,提取每个参数的接口类型和参数名
- 根据参数信息生成out参数列表、mock初始化代码和SUT实例化代码
以下是简化的实现示例:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Text; // 解析构造函数语法树 var tree = CSharpSyntaxTree.ParseText(@"public class MyService { public MyService(IFooStore store, IBarService service) { } }"); var root = await tree.GetRootAsync(); var ctor = root.DescendantNodes().OfType<ConstructorDeclarationSyntax>().First(); var parameters = ctor.ParameterList.Parameters; // 生成MakeSut方法代码 var sb = new StringBuilder(); sb.AppendLine($"private static {ctor.Parent.Identifier.Text} MakeSut("); sb.AppendLine(string.Join(",\n", parameters.Select(p => $" out Mock<{p.Type}> mock{p.Identifier.Text.Substring(0,1).ToUpper() + p.Identifier.Text.Substring(1)}"))); sb.AppendLine(") {"); // 初始化所有mock foreach (var p in parameters) { var mockName = $"mock{p.Identifier.Text.Substring(0,1).ToUpper() + p.Identifier.Text.Substring(1)}"; sb.AppendLine($" {mockName} = new();"); } // 实例化SUT sb.AppendLine($" return new {ctor.Parent.Identifier.Text}("); sb.AppendLine(string.Join(",\n", parameters.Select(p => $" mock{p.Identifier.Text.Substring(0,1).ToUpper() + p.Identifier.Text.Substring(1)}.Object"))); sb.AppendLine(" );"); sb.AppendLine("}"); Console.WriteLine(sb.ToString());
可以把这段逻辑封装成控制台工具、VS扩展或者源代码生成器(Source Generator),在编译时自动为目标类生成MakeSut方法。
二、使用AutoFixture + AutoMoq简化测试
如果不想手动生成MakeSut,可以用AutoFixture结合AutoMoq扩展,自动处理依赖注入并获取模拟对象:
// 初始化Fixture并启用AutoMoq var fixture = new Fixture().Customize(new AutoMoqCustomization()); // 创建SUT,AutoFixture会自动创建所有依赖的Mock var sut = fixture.Create<MyService>(); // Freeze方法获取并固定对应的Mock对象,后续可进行Setup/Verify var mockStore = fixture.Freeze<Mock<IFooStore>>(); // 测试逻辑 mockStore.Setup(s => s.GetFoo(It.IsAny<int>())).ReturnsAsync(new Foo()); sut.DoTheThing(); mockStore.Verify(s => s.GetFoo(It.IsAny<int>()), Times.Once);
这种模式完全不需要手动编写依赖初始化代码,AutoFixture会自动处理所有依赖,Freeze方法可以精准获取需要操作的模拟对象。
三、测试类字段存储Mock的替代模式
如果偏好更直观的写法,可以在测试类中声明所有Mock字段,在SetUp方法中初始化SUT:
public class MyServiceTests { private Mock<IFooStore> _mockStore = new(); private Mock<IBarService> _mockService = new(); private MyService _sut; [SetUp] public void Setup() { // 重置Mock状态,避免测试间污染 _mockStore.Reset(); _mockService.Reset(); _sut = new MyService(_mockStore.Object, _mockService.Object); } [Test] public void DoTheThing_ShouldCallGetFoo() { // arrange _mockStore.Setup(s => s.GetFoo(It.IsAny<int>())).ReturnsAsync(new Foo()); // act _sut.DoTheThing(); // assert _mockStore.Verify(s => s.GetFoo(It.IsAny<int>()), Times.Once); } }
这种模式适合测试类中所有测试都依赖同一组模拟对象的场景,不需要每次调用MakeSut,但需要注意在SetUp中重置Mock状态。
内容的提问来源于stack exchange,提问作者Patrick Szalapski
相关产品推荐
相关产品推荐

