You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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方法。核心思路是:

  1. 解析目标类的构造函数,提取每个参数的接口类型和参数名
  2. 根据参数信息生成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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 07:34:58