AutoFixture In C#

Data Customization

We have a simple model for test:

public record TripDto(Guid Id, IEnumerable<CityDto> City, string Price, int Days) { }
public record CityDto(string Name, int Size);

Now let's introduce some constraints.

We want to have trips with amount of cities between 1 and 5, prices between "5000.5" and "9000.5", duration between 3 and 6 days.

City.Name must consist of only three capital letters

And say, we would have a Price as a string to be able to mitigate precision problems on serialization.

To create customized objects with Autofixture we need to create an class, which implements ISpecimenBuilder interface. This class will be executed every time auto-fixture will need to create new instance of Entity.

public class EntityCustomization : ISpecimenBuilder
{
    protected readonly Random Rand;

    protected AbstractCustomization(Random rand)
    {
        Rand = rand;
    }

    public object Create(object request, ISpecimenContext context)
    {
        if (request is not Type type || type != typeof(Entity))
            return new NoSpecimen(); // if a request, sent to our customization is not of type
            // we need to create, return signal, that we don't need to customize on it

        return new Entity(); // if current request is of type Entity, we can return customized instance of it
    }
}

Partially this request type check will be on each custom SpecimenBuilder. To make some generalization, let's create a base abstract class, which will accumulate some common behavior and add additional randomization logic:

public abstract class AbstractCustomization<T> : ISpecimenBuilder where T : notnull
{
    protected readonly Random Rand;

    protected AbstractCustomization(Random rand)
    {
        Rand = rand;
    }

    public object Create(object request, ISpecimenContext context)
    {
        if (request is not Type type || type != typeof(T))
            return new NoSpecimen();

        return Create(context);
    }

    // this method will be executed only in case, when request is required type
    public abstract T Create(ISpecimenContext context);

    // returns string with numeric content
    protected string GetRandomNumberString(double from, double to) => Convert.ToString(
        Rand.NextInt64((long)from, (long)(to - 1)) + Rand.NextDouble());

    // get ASCII char by it's index
    protected char GetRandomASCIIChar(int fromIndex, int toIndex) => (char)Rand.Next(fromIndex, toIndex);

    // get random int in given range
    protected int GetInt(int from, int to) => Rand.Next(from, to);

    // get string of given length from characters between specified indexes
    protected string GenerateString(int asciiIndexFrom, int asciiIndexTo, int length) => Enumerable
        .Repeat(() => GetRandomASCIIChar(asciiIndexFrom, asciiIndexTo), length)
        .Aggregate(new StringBuilder(), (acc, getChar) =>
        {
            acc.Append(getChar());
            return acc;
        })
        .ToString();
}

Now create a concrete implementations for two our DTOs:

public class TripCustomizations : AbstractCustomization<TripDto>
{
    public TripCustomizations(Random rand) : base(rand) { }

    // generate TripDto object which satisfies required boundaries:
    public override TripDto Create(ISpecimenContext context) => new(
        Id: Guid.NewGuid(),
        // create collection of cities with length between 1 and 5
        Cities: context.CreateMany<CityDto>(GetInt(1, 5)),
        // using methods, defined in abstract class to generate some randomized values
        Price: GetRandomNumberString(5000.5, 9000.5),
        Days: GetInt(3, 6)
    );
}

public class CityCustomization : AbstractCustomization<CityDto>
{
    private readonly HashSet<string> _usedCityNames = new();

    public CityCustomization(Random rand) : base(rand)
    {
    }

    public override CityDto Create(ISpecimenContext context) => new(
        // on city name we have a constraint - it has to be unique and consist of 3 capital latin letters.
        Name: GetUnique3CapitalCharCityName(),
        Size: GetInt(1, 10)
    );

    // to create every time only unique names, we will use hash set, stored as object's private member
    // every time we will need to get new name we will check if it exists in this cash and generate new
    // until we won't have a unique one. having unique name we will add it to cash and then return.
    // there may be more efficient implementation, e.g. using tries, but it is enough for now =)
    private string GetUnique3CapitalCharCityName()
    {
        var cityName = GenerateName();
        while (_usedCityNames.Contains(cityName))
        {
            cityName = GenerateName();
        }
        _usedCityNames.Add(cityName);

        return cityName;

        string GenerateName() => GenerateString(65, 90, 3);
    }
}

Now there is only one step remaining - register customizations:

var rand = new Random();
var fixture = new Fixture();
fixture.Customizations.Add(new ElementDepartureCustomization(rand));
fixture.Customizations.Add(new ElementPriceCustomization(request.DeparturesAmount, rand));

var tirps = fixture.CreateMany<TripDto>(5);

References