Introduction

When setting up test data in unit tests certain data has to be specific format or range. I have written about the formats before but today I am going to look at a specific range. When setting up a class or record type to represent a user or a client or an employee capturing the date of birth as a data point can be important. AutoFixture by default sets DateTime values to anywhere between 2 years ago and 2 years in the future. This isn’t all that helpful for dates of birth as it’s unlikely someone will be less than 2 or born in the future.

So how do we change this behaviour? Let’s a take a look.

Walkthrough

Let’s take a simple C# 9 record type and define a UserRecord. This will store basic information about the user we are trying to create in our unit test.

public record UserRecord(int Id, string Name, DateTime DateOfBirth);

This is using the new record syntax in C# 9.

As we’ve already seen previously AutoFixture works well with records so we need to adjust the DateTime generation behaviour. To adjust behaviour AutoFixture has the concept of Customizations which you can add to the Fixture instance you are using to generate your anoymous data.

var fixture = new Fixture();
fixture.Customizations.Add(new DateOfBirthDateTimeBuilder());
UserRecord user = fixture.Create<UserRecord>();

The above will now use the DateOfBirthDateTimeBuilder to generate the DateOfBirth of the UserRecord when it generates.

As you can see the customisation setup is straight forward. The power comes in the ISpecimenBuilder implementation.

AutoFixture already has a built in ISpecimenBuilder which generates DateTime values so we are going to use that to do the heavy lifting. Our implementation will specify the date ranges, between 18 and 100 years old, and determine if the call applies.

Implementation

Constructor

The functionality is split up into two sections; setting up the RandomDateTimeSequenceGenerator instance in the constructor and calling the generator in the Create method.

public DateOfBirthDateTimeBuilder()
{
    var oneHundredYearsOld = DateTime.Today.AddYears(-100);
    var eighteenYearsOld = DateTime.Today.AddYears(-18);
    _dateOfBirthGenerator = new RandomDateTimeSequenceGenerator(oneHundredYearsOld, eighteenYearsOld);
}

In the constructor you can see above we are specifying the age range, between 18 and 100 years old, working from today. These values are then used to create an instance of the RandomDateTimeSequenceGenerator which will be used in the Create method. The generator requires a min and max date for the range so the above is specifying 100 years ago as the min and 18 years ago as the max.

Create

The Create method is where we have to determine whether we need to apply the changes or just let the generation pipeline continue. This uses a bit of reflection to determine if we’re at the right place in the object graph which is being constructed and then calls the DateOfBirthDateTimeBuilder.

public object Create(object request, ISpecimenContext context)
{
    var pi = request as PropertyInfo;
    if (pi == null || pi.PropertyType != typeof(DateTime))
    {
        return new NoSpecimen();
    }

    if (pi.Name == nameof(UserRecord.DateOfBirth))
    {
        return _dateOfBirthGenerator.Create(typeof(DateTime), context);
    }

    return new NoSpecimen();
}

As you can see from above we want to determine whether the checks are applicable as quickly as possible. Now this isn’t code which is going to be run by a user however if unit tests don’t run quickly developers will find a reason to ignore them! If the request parameter is null or as a PropertyInfo the type isn’t DateTime then we short circuit the call and tell AutoFixture that NoSpecimen has been found and to carry on with the other out of the box customisations.

If the condition is satisfied then we can check that the name of the PropertyInfo is the one we are looking for otherwise it will also return NoSpecimen. If all the conditions are successful it will call into the date of birth generator.

Some of the eagle eyed among you will have noticed that the multiple conditional checks are not ideal and can be combined. Yes they can but I wanted to show the logic flow in a straight forward way to demonstrate how to determine the values required first. Below is the updated version.

Final Implementation

public class DateOfBirthDateTimeBuilder : ISpecimenBuilder
{
    private readonly RandomDateTimeSequenceGenerator _dateOfBirthGenerator;

    public DateOfBirthDateTimeBuilder()
    {
        var oneHundredYearsOld = DateTime.Today.AddYears(-100);
        var eighteenYearsOld = DateTime.Today.AddYears(-18);
        _dateOfBirthGenerator = new RandomDateTimeSequenceGenerator(oneHundredYearsOld, eighteenYearsOld);
    }

    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as PropertyInfo;

        if (pi?.PropertyType == typeof(DateTime)
            && pi.Name == nameof(UserRecord.DateOfBirth))
        {
            return _dateOfBirthGenerator.Create(typeof(DateTime), context);
        }

        return new NoSpecimen();
    }
}

As you can see comparing the “Final Implementation” to the original Create method implementation we are still exiting early if null, we’re still existing early if the property type is not DateTime and then we’re finally checking for the required name. Only then are we happy to call the date of birth generator. This implementation works on properties called “DateOfBirth” only however the property name checking can be extended to check other variations as required.

Conclusion

In this post we have touched on writing a custom ISpecimenBuilder implementation to solve the problem of creating valid values for “Dates of Birth” when creating anonymous test data. If you’re interested in AutoFixture then please check out my other posts on the subject.

As always any questions/comments/suggestions the please get in contact with me on Twitter @WestDiscGolf.