使用Autofixture将子实例的属性值设置为固定值

29

在使用Autofixture构建父类并添加默认值到子类的所有属性时,是否有可能为子实例上的某个属性分配一个固定值?它可以轻松地为子实例上的所有属性添加默认值,但我想要覆盖并为子实例上的某个属性分配一个特定的值。

考虑到这种父/子关系:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public int Number { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}
我希望将地址实例的城市属性赋予特定值。我在考虑这个测试代码:
var fixture = new Fixture();

var expectedCity = "foo";

var person = fixture
    .Build<Person>()
    .With(x => x.Address.City, expectedCity)
    .Create();

Assert.AreEqual(expectedCity, person.Address.City);

那是不可能的。我猜测,这是由于反射异常所引起的。

System.Reflection.TargetException : Object does not match target type.

...Autofixture尝试将该值分配给Person实例的City属性,而不是Address实例。

有什么建议吗?

是的,我知道我可以添加一个额外的步骤,像下面这样:

var fixture = new Fixture();

var expectedCity = "foo";

// extra step begin
var address = fixture
    .Build<Address>()
    .With(x => x.City, expectedCity)
    .Create();
// extra step end

var person = fixture
    .Build<Person>()
    .With(x => x.Address, address)
    .Create();

Assert.AreEqual(expectedCity, person.Address.City);

...但是我希望使用第一个版本或类似的东西(步骤更少,更简洁)。

注意:我正在使用Autofixture v3.22.0。


相关:https://dev59.com/nmLVa4cB1Zd3GeqPvmDo#11657881 - Ruben Bartelink
3个回答

27

为了完整起见,这里还有另一种方法:

fixture.Customize<Address>(c => 
    c.With(addr => addr.City, "foo"));

var person = fixture.Create<Person>();

这将自定义所有 Address 实例的创建:

如果您经常使用此功能,则可以考虑将其包装在 ICustomization 中:

public class AddressConventions : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<Address>(c => 
            c.With(addr => addr.City, "foo"));
    }
}

fixture.Customize(new AddressConventions());

12

不是为了对问题不屑一顾,但最简单的解决方案可能实际上就是这样:

[Fact]
public void SimplestThingThatCouldPossiblyWork()
{
    var fixture = new Fixture();
    var expectedCity = "foo";
    var person = fixture.Create<Person>();
    person.Address.City = expectedCity;
    Assert.Equal(expectedCity, person.Address.City);
}

将明确的值分配给属性是大多数编程语言已经非常擅长的(C#当然也是),因此我认为AutoFixture不需要一个复杂的DSL来复制其中一半的功能。


1
虽然对于我们大多数人来说,在C#中为属性分配显式值是(几乎)毫不费力的,但最初的问题是:我是否可以在父/子关系中使用Autofixture为子对象的属性分配固定值。 提出这个问题的原因仅仅是出于美学考虑,因为我认为使用.With(...)语法将值分配分组会更“漂亮”,尤其是在有多个分配时(尽管这是主观的)。 除了美学之外,您的答案完成了工作。 - Jens Andresen
通过实现自定义的 ISpecimenBuilder,您可以固定一个值,但是当它是一个"Train Wreck"属性时,我认为不能使用流畅的 DSL。从问题中我确实感觉到您想要漂亮的方式,但如果您想要基于约定的方法,我很乐意分享它。 - Mark Seemann
不用了,谢谢。这个“火车失事”停在我的应用程序的最外层边界上,并且只是模拟一个传入的数据结构,所以我会接受一些测试案例看起来...嗯...不太好看的情况;-) - Jens Andresen
我遇到了这个问题,因为我也有同样的问题。在我看来,AF 的一个不错的特性之一就是能够使用流畅的 API。让我感到惊讶的是 AF 不支持这个特性。既然我能够流畅地编写代码,那么它应该可以工作。对我来说,这是一个“最少惊奇原则”的案例。 - Flodpanter
遗憾的是,这对我来说不是一个选项。如果你在构造函数中有验证,这将无法工作。 - IamDOM
4
2020年已经不存在公共Setter的说法。 - Nick

-1
您可以在构建器上使用 Do 方法:
var person = this.fixture
               .Build<Person>()
               .Do( x => x.Address.City = expectedCity )
               .Create();

你也可以将你的实例注入到夹具并冻结:

this.fixture.Inject( expectedCity );

你有试过运行那段代码吗?我运行它时会抛出一个 NullReferenceException 异常。 - Jens Andresen
...而Inject会将所有字符串属性设置为expectedCity的值 - 这并不是我想要的。 - Jens Andresen
确实,Do 方法出现了 NullReferenceException 异常,我认为这可以被视为一个 bug,因为该方法来自名为 IPostprocessComposer<T> 的接口,所以后处理应该在对象及其依赖项创建之后进行。 - sgnsajgon
最简单的解决方案就是在对象创建后分配语句:person.Address.City = expectedCity; - sgnsajgon
看起来你是对的,这是一个 bug。根据它的设计者博客(https://blog.ploeh.dk/2009/06/09/CallingMethodsWhileBuildingAnonymousVariablesWithAutoFixture/),"你可以混合使用 With 和 Do,ObjectBuilder<T> 将保留顺序。" - bkqc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接