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

时间:2018-01-08 13:04:32

标签: c# unit-testing autofixture

我希望能够使用ICustomization基于ISpecimenBuilder.CreateMany生成不同的值。我想知道什么是最好的解决方案,因为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>>()似乎非常直观,因为它有点违背了{​​{1}}期望的目的;这将生成硬编码大小CreateMany<Foo>(?)。另一个缺点是我必须为每个实体进行两次自定义;一个用于创建自定义集合,另一个用于创建单个实例,因为我们正在覆盖List<List<Foo>>创建集合的行为。

PS。:主要目标是减少测试中的代码量,因此我必须避免调用Create<T>来自定义每个测试的值。有没有正确的方法呢?

1 个答案:

答案 0 :(得分:0)

一般来说,像这样的问题会引发一些警钟,但我会先给出一个答案,然后将这些警告保存到最后。

如果你想要不同的价值观,依靠随机性不是我的第一选择。随机性的问题在于,有时候,随机过程会连续两次挑选(或产生)相同的值。显然,这取决于人们想要选择的范围,但即使我们认为像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;
    }
}

这个泛型类只是一个概念证明。如果你想从一个大型池中绘制,那么必须用数百万个值初始化它可能效率低下,但在这种情况下,你可以改变实现,以便对象以空值列表开始,然后添加每次调用Draw时,每个随机生成的值都会显示为“已使用”对象列表。

要为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);
// ...

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

根据我的经验,如果这些自定义对于测试套件中的大多数测试都有意义,那么应该只向自动混合添加自定义。如果您为所有测试添加自定义项只是为了支持一个或两个测试的测试用例,那么您很容易得到脆弱且不可维护的测试。

相关问题