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

标签 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, $"<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="60030f0e140103140d05200d01090c4e030f0d" rel="noreferrer noopener nofollow">[email protected]</a>")
            .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;
    }
}

这个泛型类只是一个概念证明。如果您希望从一个大池中提取数据,则必须使用数百万个值进行初始化可能效率很低,但在这种情况下,您可以更改实现,使对象以空值列表开头,然后添加每次将每个随机生成的值添加到“已使用”对象列表 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));
}

所有三个测试均通过。


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

想要独特值(value)观的动机是什么?

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

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

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

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

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

根据我的经验,只有当这些自定义对测试套件中的大多数测试有意义时,才应将自定义添加到 AutoFixture。如果您向所有测试添加自定义只是为了支持一两个测试的测试用例,那么您很容易得到脆弱且难以维护的测试。

关于c# - 基于定制的随机样本生成方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48150899/

相关文章:

javascript - 如何处理影响 Jest 中其他测试的全局对象/测试

c# - 如何处理复选框列表?

c# - 如何在 MVC5 中为 Bundle 启用缓存

javascript - React 中如何模拟组件

c# - AutoFixture 单元测试参数设置

unit-testing - 为什么 AutoFixture 自定义会导致未填充继承的属性?

c# - AutoFixture重构

c# - 从字符串中删除以 SOT 开头并以 EOT 结尾的子字符串

c# - 在 Application Insight .Net Core 2.1 中使用自定义属性记录用户名和 IP

node.js - 如何在 Jest 中测试来自 Express 的响应数据