问题
I want to be able to generate distinct values based on a ICustomization
using ISpecimenBuilder.CreateMany
. I was wondering what would be the best solution since AutoFixture will generate the same values for all entities.
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);
}
}
I've read this and that might be what I'm looking for. This approach has many drawbacks though: First it seems really counter intuitive to call Create<List<Foo>>()
because it kinda of defeats the purpose of what to expect from CreateMany<Foo>
; that would generate a hardcoded sized List<List<Foo>>
(?). Another drawback is that I'd have to have two customizations for each entity; one to create custom collections and another one to create a single instance, since we are overriding the behaviour of Create<T>
to create a collections.
PS.: The main goal is to reduce the amount of code on my tests, so I must avoid calling With()
to customize the values for every test. Is there a proper way of doing this?
回答1:
In general, a question like this triggers some alarm bells with me, but I'll give an answer first, and then save the admonitions till the end.
If you want distinct values, relying on randomness wouldn't be my first choice. The problem with randomness is that sometimes, a random process will pick (or produce) the same value twice in a row. Obviously, this depends on the range from which one wants to pick, but even if we consider that something like Guid.NewGuid().ToString().Substring(0, 6))
would be sufficiently unique for our use case, someone could come by later and change it to Guid.NewGuid().ToString().Substring(0, 3))
because it turned out that requirements changed.
Then again, relying on Guid.NewGuid()
is good enough to ensure uniqueness...
If I interpret the situation here correctly, though, Identifier
must be a short string, which means that you can't use Guid.NewGuid()
.
In such cases, I'd rather guarantee uniqueness by creating a pool of values from which you can draw:
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;
}
}
This generic class is just a proof of concept. If you wish to draw from a big pool, having to initialise it with, say, millions of values could be inefficient, but in that case, you could change the implementation so that the object starts with an empty list of values, and then add each randomly generated value to a list of 'used' objects each time Draw
is called.
To create unique identifiers for Foo
, you can customise AutoFixture. There's many ways to do this, but here's a way using an 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();
}
}
Add this to a Fixture
object, and it'll create Foo
objects with unique Identifier
properties until the pool runs dry:
[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));
}
All three tests pass.
All that said, though, I wish to add some words of caution. I don't know what your particular scenario is, but I'm also writing these warnings to other readers who may happen by this answer at a later date.
What's the motivation for wanting unique values?
There can be several, and I can only speculate. Sometimes, you need values to be truly unique, for example when you're modelling domain Entities, and you need each Entity to have a unique ID. In cases like that, I think this is something that should be modelled by the Domain Model, and not by a test utility library like AutoFixture. The easiest way to ensure uniqueness is to just use GUIDs.
Sometimes, uniqueness isn't a concern of the Domain Model, but rather of one or more test cases. That's fair enough, but I don't think it makes sense to universally and implicitly enforce uniqueness across all unit tests.
I believe that explicit is better than implicit, so in such cases, I'd rather prefer to have an explicit test utility method that would allow one to write something like this:
var foos = fixture.CreateMany<Foo>();
fixture.MakeIdentifiersUnique(foos);
// ...
This would allow you to apply the uniqueness constraints to those unit tests that require them, and not apply them where they're irrelevant.
In my experience, one should only add Customizations to AutoFixture if those Customizations make sense across a majority of the tests in a test suite. If you add Customizations to all tests just to support a test case for one or two tests, you could easily get brittle and unmaintainable tests.
来源:https://stackoverflow.com/questions/48150899/approach-to-generate-random-specimen-based-on-customization