基于定制的随机样本生成方法

Approach to generate Random specimen based on Customization

我希望能够使用 ISpecimenBuilder.CreateMany 基于 ICustomization 生成不同的值。我想知道最好的解决方案是什么,因为 AutoFixture 将为所有实体生成相同的值。

public class FooCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var specimen = fixture.Build<Foo>()
            .OmitAutoProperties()
            .With(x => x.CreationDate, DateTime.Now)
            .With(x => x.Identifier, Guid.NewGuid().ToString().Substring(0, 6)) // Should gen distinct values
            .With(x => x.Mail, $"contactme@mail.com")
            .With(x => x.Code) // Should gen distinct values
            .Create();

            fixture.Register(() => specimen);
    }
}

我已阅读 this,这可能正是我要找的。但是这种方法有很多缺点:首先,调用 Create<List<Foo>>() 似乎违反直觉,因为它有点违背了 CreateMany<Foo> 的预期目的;这将生成一个硬编码大小的 List<List<Foo>> (?)。另一个缺点是我必须为每个实体进行两次自定义;一个用于创建自定义集合,另一个用于创建单个实例,因为我们正在覆盖 Create<T> 的行为以创建集合。

PS.: 主要目标是减少我测试的代码量,所以我必须避免调用 With() 为每个测试自定义值。有正确的方法吗?

总的来说,这样的问题会给我敲响警钟,但我会先给出答案,然后再警告。

如果你想要不同的值,依赖随机性不是我的第一选择。随机性的问题在于,有时随机过程会连续两次选择(或产生)相同的值。显然,这取决于人们想要选择的范围,但即使我们认为 Guid.NewGuid().ToString().Substring(0, 6)) 之类的东西对于我们的用例来说足够独特,稍后有人可能会过来并将其更改为 Guid.NewGuid().ToString().Substring(0, 3))因为事实证明需求发生了变化。

再说一遍,靠Guid.NewGuid()就可以保证唯一性了...

不过,如果我对这里的情况的解释正确,Identifier 必须是一个短字符串,这意味着您不能使用 Guid.NewGuid().

在这种情况下,我宁愿通过创建一个您可以从中提取的值池来保证唯一性:

public class RandomPool<T>
{
    private readonly Random rnd;
    private readonly List<T> items;

    public RandomPool(params T[] items)
    {
        this.rnd = new Random();
        this.items = items.ToList();
    }

    public T Draw()
    {
        if (!this.items.Any())
            throw new InvalidOperationException("Pool is empty.");

        var idx = this.rnd.Next(this.items.Count);
        var item = this.items[idx];
        this.items.RemoveAt(idx);
        return item;
    }
}

这个通用 class 只是一个概念证明。如果你想从一个大池中提取,必须用例如数百万个值初始化它可能效率低下,但在这种情况下,你可以更改实现,使对象以一个空值列表开头,然后添加每次调用 Draw 时,每个随机生成的值都会添加到 'used' 个对象的列表中。

要为 Foo 创建唯一标识符,您可以自定义 AutoFixture。有很多方法可以做到这一点,但这里有一种使用 ISpecimenBuilder:

的方法
public class UniqueIdentifierBuilder : ISpecimenBuilder
{
    private readonly RandomPool<string> pool;

    public UniqueIdentifierBuilder()
    {
        this.pool = new RandomPool<string>("foo", "bar", "baz", "cux");
    }

    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as PropertyInfo;
        if (pi == null || pi.PropertyType != typeof(string) || pi.Name != "Identifier")
            return new NoSpecimen();

        return this.pool.Draw();
    }
}

将此添加到 Fixture 对象,它将创建 Foo 个具有独特 Identifier 属性的对象,直到池干涸:

[Fact]
public void CreateTwoFooObjectsWithDistinctIdentifiers()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new UniqueIdentifierBuilder());

    var f1 = fixture.Create<Foo>();
    var f2 = fixture.Create<Foo>();

    Assert.NotEqual(f1.Identifier, f2.Identifier);
}

[Fact]
public void CreateManyFooObjectsWithDistinctIdentifiers()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new UniqueIdentifierBuilder());

    var foos = fixture.CreateMany<Foo>();

    Assert.Equal(
        foos.Select(f => f.Identifier).Distinct(),
        foos.Select(f => f.Identifier));
}

[Fact]
public void CreateListOfFooObjectsWithDistinctIdentifiers()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new UniqueIdentifierBuilder());

    var foos = fixture.Create<IEnumerable<Foo>>();

    Assert.Equal(
        foos.Select(f => f.Identifier).Distinct(),
        foos.Select(f => f.Identifier));
}

三个测试全部通过。


尽管如此,我还是想补充一些警告。我不知道您的具体情况是什么,但我也将这些警告写给以后可能会遇到此答案的其他读者。

想要独特价值的动机是什么?

可以有几个,我只能推测。有时,您需要真正唯一的值,例如当您对域实体建模时,您需要每个实体都有一个唯一的 ID。在这种情况下,我认为这应该由领域模型建模,而不是由像 AutoFixture 这样的测试实用程序库建模。确保唯一性的最简单方法是只使用 GUID。

有时,唯一性不是领域模型的关注点,而是一个或多个测试用例的关注点。这很公平,但我认为在所有单元测试中普遍和隐含地强制执行唯一性是没有意义的。

我相信 explicit is better than implicit,所以在这种情况下,我宁愿有一个明确的测试实用程序方法,它允许人们编写如下内容:

var foos = fixture.CreateMany<Foo>();
fixture.MakeIdentifiersUnique(foos);
// ...

这将允许您将唯一性约束应用于那些需要它们的单元测试,而不是在它们不相关的地方应用它们。

根据我的经验,如果自定义对测试套件中的大多数测试都有意义,则应该只将自定义添加到 AutoFixture。如果您将自定义添加到所有测试只是为了支持一个或两个测试的测试用例,您很容易得到脆弱且无法维护的测试。