【问题标题】:Approach to generate Random specimen based on Customization基于定制的随机样本生成方法
【发布时间】:2018-01-08 13:04:31
【问题描述】:

我希望能够使用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&lt;List&lt;Foo&gt;&gt;() 似乎真的违反直觉,因为它有点违背了对CreateMany&lt;Foo&gt; 的期望;这将生成一个硬编码大小的List&lt;List&lt;Foo&gt;&gt; (?)。另一个缺点是我必须为每个实体进行两次自定义;一个用于创建自定义集合,另一个用于创建单个实例,因为我们将覆盖 Create&lt;T&gt; 的行为来创建集合。

PS.:主要目标是减少我的测试代码量,所以我必须避免调用With() 来自定义每个测试的值。有没有合适的方法来做到这一点?

【问题讨论】:

标签: c# unit-testing autofixture


【解决方案1】:

一般来说,像这样的问题会给我敲响一些警钟,但我会先给出答案,然后把训诫留到最后。

如果您想要不同的值,依赖随机性不是我的首选。随机性的问题在于,有时,随机过程会连续两次选择(或产生)相同的值。显然,这取决于人们想要选择的范围,但即使我们认为像 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 对象,它将创建具有唯一 Identifier 属性的 Foo 对象,直到池干涸:

[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。如果您将自定义添加到所有测试中只是为了支持一个或两个测试的测试用例,您很容易得到脆弱和不可维护的测试。

【讨论】:

  • 我真的很欣赏详细而仔细的回复,我想添加一些 cmets:关于唯一值:我正在测试一种批量插入 Foo's 的方法。我的域验证“标识符”是唯一的(输入了它的值,所以我不能只使用 GUID)。关于定制 AutoFixture;我认为这正是这里的情况。我将ICustomization 用于Foo,因为Foo 对于此上下文中的所有测试都是完全相同的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-02-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-10
  • 2011-07-27
  • 2020-09-08
  • 1970-01-01
相关资源
最近更新 更多