Introduction

When writing unit tests setting up the Arrange phase for the data can be frustrating at times. When the POCO changes it can break your tests. This can lead to developers ignoring tests early on in new projects as more structure is in flux and this can lead to tests being ignored completely. There are tools out there which can help with that such as AutoFixture. This is a library which aids with setting up anonymous data for tests and sets random values for the properties of the object instances. This allows for tests to only concentrate on the items and properties it cares about. This means that if some change or refactor occurs and breaks the tests then it will be more likely the test needs to change as well.

I was reading this blog post the other day - https://kaca.hashnode.dev/easily-create-mock-data-for-unit-tests - which goes onto explain how using the Builder Pattern you can easily create mock data for your unit tests. It does however rely on you to setup the base data yourself. This is where combining this technique with AutoFixture you can get the benefits of both worlds; anonymous data from AutoFixture and readability through setting explicit values per test using the builder pattern.

What is the Builder Pattern?

The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of > the Builder design pattern is to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns.

From https://en.wikipedia.org/wiki/Builder_pattern

But what does this mean? In a nutshell it gives you a “builder” which is responsible for creating an instance of an object in a valid state to then use in further processing instead of instantiating a “new” instance directly. To find when a POCO or record type is to be created through a builder pattern in your application code the default constructor maybe marked as private depending on coding standards. It may also have a factory for instantiation.

AutoFixture is a Builder

As many of you are aware AutoFixture is already a “Builder” of sorts so why would you want to add complexity into your test setup? Well yes AutoFixture can be seen as a Builder. It may do some “magic” under the hood and may not use the same terminology but you are building an instance of your POCO. The difference is you are allowing AutoFixture to determine the values and in some tests you want to be explicit.

Being explicit and setting a value directly on a POCO is fine, you have a setter on a property you want to specify a value for and set it. However you can’t do that with C# 9 Record types as they are immutable.

C# 9 Records

For the rest of this blog post all of the examples and code will refer back to the following record type.

public record UserRecord (int Id, string Name, string Email, DateTime DateOfBirth, bool IsDeleted = false, DateTime? DeletedOn = default);

As you can see from the above we have a simple user record defined type. This is based on Dino Kacavenda’s example but converted into a Record type.

As we’ve previously seen AutoFixture and Record types work well together - click here - so getting AutoFixture to create anonymous data instances is fine. However due to the nature of record types we can’t just set a specific value after it has been instantiated and we have to use a different method.

To do this with record types the “with” syntax needs to be used. This allows a new version of the record to be created however with the different values from the original where specified in the with.

[Fact]
public void Set_Id()
{
    // Arrange
    UserRecord user = new Fixture().Create<UserRecord>();
    user = user with { Id = 1 };

}

From the above we are asking AutoFixture to create a new UserRecord instance. This will be constructed with anonymous data and the Id property will have an arbitrary integer value. For this example we explicitly want to the Id to be 1 so we create a new record with that value.

Extension Methods

If we continued down this route and started setting other properties to explicit values this syntax could get a little messy relatively quickly and when it comes to unit tests we need to keep the readability high so the cognitive load on the developers maintaining the code doesn’t get too much. With saying that we can encapsulate this processing into an extension method and this is the beginning of our builder pattern.

public static UserRecord WithId(this UserRecord user, int id) => user with { Id = id };

As you can see we are putting the same code inside the extension method. We can then specify the method to allow for readability when being consumed.

[Fact]
public void Set_Id_WithBuilder()
{
    // Arrange
    UserRecord user = new Fixture().Create<UserRecord>();
    user = user.WithId(1);

}

None Simple Properties

One of the properties for the record is a string to represent an email address. As everyone knows an email address can not be any string however unless some additional configuration is applied AutoFixture does not know this. To give AutoFixture a hand we can add a customization into the Fixture so that it knows how to create email addresses when required.

[Fact]
public void Set_EmailAddress()
{
    // Arrange
    var fixture = new Fixture();
    fixture.Customize<UserRecord>(c =>
        c.With(x => x.Email, fixture.Create<MailAddress>().Address)
    );
    UserRecord user = fixture.Create<UserRecord>();
    
}

This will make sure that when a new UserRecord is created it will be in the format of <string>@example.com in this scenario.

But what happens when we want to set the value on the record type for the specific test. Again an extension method to the rescue.

public static UserRecord WithEmail(this UserRecord user, string emailAddress) => user with { Email = emailAddress };

Keeping Data in a Consistent State

Now you might be wondering “What do these extension methods give me over the ‘with’ syntax on record types?” and that is a valid question. At the end of the day it does just come down to personal preference. However the power comes when you start to make sure certain data is in a consistent state as well as the ability to chain them together.

As part of your application you may have a delete function which under the hood is a “soft” delete where it sets a IsDeleted flag. As part of this it also has a corresponding value of when it was deleted. If we continue down the road of only using the “with” syntax every time a developer wants to set the UserRecord to be deleted they have to remember to also set the deleted on value. As the scenarios get more complicated, time pressures rise and developers come/go on the project these types of things are forgotten about and can introduce bugs.

With the builder extension methods you can start to mitigate this by making sure the values are set together and kept inline for the test.

public static UserRecord IsDeleted(this UserRecord user, DateTime? deletedOn = default)
{
    return user with { IsDeleted = true, DeletedOn = deletedOn ?? DateTime.UtcNow };
}

This can then be used in the test method with an instantiated UserRecord.

user = user.IsDeleted();

Joining Them Together

Now the whole benefit of the builder pattern is you can specify multiple items together and then “build” the outcome. Using the extension methods and the C#9 record types you don’t have to “build” at the end of the process as returning from each extension method you get a new version. This is why I wouldn’t recommend using this approach for application data construction but I feel the trade off with readability is acceptable for unit tests with record types.

With a Record type definition, or POCO for that matter, you would be looking to have an extension method to work on one area and one area only; remember the Single Responsibility Principle applies to this level of functionality as well. I say “area” as this covers the scenario of IsDeleted and optionally setting the datetime it was deleted on.

An example of chaining them together could look like this.

user = user
        .WithName("Adam Storr")
        .WithEmail("adam@example.com")
        .IsDeleted();

As you can see from the above we are starting with a user record created with anonymous daya from AutoFixture. We can then explicitly set the values for our targetted items through the builder style extension methods. All the other data points are set to anonymous values as before so we have a valid deleted user record to be used in the unit test.

Conclusion

In this post we have reviewed using the Builder pattern in conjunction with AutoFixture created anonymous records to allow for improved control over data setup in the Arrange phase of unit tests. We have looked at the maintainability of using the extension methods throughout the tests to reduce cognative load even though there maybe some performance overhead. This is the trade off to be used in unit tests and I’m happy with that.

Let me know your thoughts on this technique and what else you could use it for via Twitter @WestDiscGolf.