【问题标题】:Generating copies with Autofixture使用 Autofixture 生成副本
【发布时间】:2026-02-05 12:25:01
【问题描述】:

我正在使用 Autofixture 进行单元测试,并使用自动生成的数据。

为了测试一个简单的控制器端点(通过 Id 获取员工),我正在做类似的事情:

[Theory, AutoData]
public void GetEmployeeById_ValidId_ReturnsExpectedModel(
    EmployeeModel expectedEmployee,
    [Frozen] Mock<IEmployeeService> employeeServiceMock,
    EmployeesController sut)
{
    employeeServiceMock
        .Setup(x => x.GetEmployeeById(42))
        .Returns(expectedEmployee);

    var actual = sut.GetEmployeeById(42);

    actual.As<OkObjectResult>().Value.As<EmployeeModel>()
        .Should().BeEquivalentTo(expectedEmployee);
}

还有控制器:

[HttpGet("{id:int}")]
public IActionResult GetEmployeeById(int id)
{
    var employee = employeeService.GetEmployeeById(id);
    if (employee == null)
        return NotFound("Employee not found");

    return Ok(employee);
}

在此单元测试中,expectedEmployee 是使用“随机”数据自动生成的。 sut(被测系统)配置为生成所有必需的依赖项(其中之一是IEmplyeeService)。

这个单元测试的问题是,如果我在从控制器返回之前更改了员工,测试仍然会通过(因为它引用的是同一个对象):

employee.SomeInternalModel.FooProperty = "Foo";
return Ok(employee);

所以,我认为上面的单元测试很糟糕。

要让单元测试在这种情况下失败,我需要传递一个单独的对象:EmployeeModel 的深拷贝:

employeeServiceMock
    .Setup(x => x.GetEmployeeById(42))
    .Returns(expectedEmployee.DeepCopy());

我没有时间和资源为我的所有模型编写深拷贝方法。

如何轻松自动生成相同的模型?我考虑过播种AutoFixture,但它似乎不支持此功能。

你有什么优雅的建议吗?

【问题讨论】:

  • 如果您的模型是简单的 POCO,那么深层复制方法可以像使用 JSON.Net 的 public static T DeepCopyPoco&lt;T&gt;(this T item) =&gt; JsonConvert.DeserializeObject&lt;T&gt;(JsonConvert.SerializeObject(item)); 一样简单。

标签: c# unit-testing asp.net-core autofixture


【解决方案1】:

我认为您需要问一个问题您在测试什么?在您的 testcase 中,您只是测试 SUT 是否返回由 service 返回的 employee; IMO 它是否是同一个实例并不重要。更新属性不应破坏 this 测试。

尽管您触及了一个更广泛的问题,但在其他情况下,您确实希望将 expectedstructural equality 进行比较,在这种情况下,您可以(例如使用 xUnits MemberData)使用一个builder,当你调用它两次时会生成两个实例:

var employee = new EmployeeModelBuilder().Build();

可以使用With() 方法增强这样的构建器:

var employee = new EmployeeModelBuilder().With(name: "John").Build();

或者您可以使用 new EmployeeModel {} 内联创建这些对象。

结构相等意味着您需要一个覆盖Equality 成员的对象或在您的断言中使用IEqualityComparer&lt;&gt;

更新

如果您不想使用自定义构建器(如您所说),您可以指示 AutoFixture 生成具有特定属性设置值的对象。如果您随后要求它创建一个实例两次(一次用于您的expected,一次用于注入到您的SUT 的服务返回的实例)您可以将expected 与@ 进行比较987654340@ 在您的Assert 阶段。

    [Fact]
    public void Sut_ReturnsEmployee_FromService()
    {
        var fixture = new Fixture();
        fixture.Customize<EmployeeModel>(e => e.With(x => x.Name, "Foo"));
        var expected = fixture.Create<EmployeeModel>();

        var foundEmployee = fixture.Create<EmployeeModel>();
        var employeeServiceMock = new Mock<IEmployeeService>();
        employeeServiceMock.Setup(f => f.GetEmployeeById(42)).Returns(foundEmployee);

        var sut = new EmployeeController(employeeServiceMock.Object);

        var actual = sut.GetEmployeeById(42);

        Assert.Equal(expected.Name, actual.Name);
    }

这里我使用了[Fact] 并且断言比较了两个特定属性的相等性,但是当您比较结构相等性时,您可以只比较对象本身(如上所述)。 现在您可以验证您的SUT 是否返回了预期的实例,而不会篡改,而无需使用对同一实例的两个引用。

【讨论】:

  • 目前我们正在从构建器转移到 Autofixture。我真的希望使用自动生成功能。我看不出冻结在这里有什么帮助。