一般来说,像这样的问题会给我敲响一些警钟,但我会先给出答案,然后把训诫留到最后。
如果您想要不同的值,依赖随机性不是我的首选。随机性的问题在于,有时,随机过程会连续两次选择(或产生)相同的值。显然,这取决于人们想要选择的范围,但即使我们认为像 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。如果您将自定义添加到所有测试中只是为了支持一个或两个测试的测试用例,您很容易得到脆弱和不可维护的测试。