TL;DR Writing automated tests for your complex scenarios is important, but it’s easy for the tests to become difficult for other developers (including your 6-month future-self) to read and understand. Using a builder method for the “happy path” that returns an object representing the test context helps significantly reduce the amount of boilerplate set up in the tests and highlights the key differences a particular method is testing.

There was a time in my software development career that I dreaded automated developer tests (e.g. unit tests, executable specs, etc.) I was totally sold on the value of creating tests – improving the design of my code by forcing me to structure it in a way that separate parts could be tested both individually and as a whole; nudging myself to pause and consider the edge cases that might need to be handled; and, of course, reveling in the inner thrill of seeing the green check marks of all my passing tests.

The pain would come later, in the form of needing to maintain those test cases in the future. This typically means you have to carefully decipher the single-line difference between tests for the various outcomes that should or shouldn’t happen. Changing how a certain object is created? Get ready to fix not only the tests for that particular object, but also the tests for every object that depends on it. Hello, hours of time lost to fixing automated tests instead of working on features.

Since then I’ve learned how to use the builder pattern to solve both problems: making the variations between individual test cases clearer and reducing dependencies that end up coupling unrelated test fixtures.

The builder pattern was first defined in the book Design Patterns: Elements of Reusable Software as having an “intent to separate the construction of an object from its representation.” In simpler words: provide a way to create objects without having to do it yourself. There are several ways to implement the builder pattern, but they all share this same purpose.

Let’s take a look at some code. First, we’ll show a test without the builder pattern and talk about how it can become broken by future changes, and then look at a simple builder pattern that helps protect us from those future changes.

Test without a builder
    class NoBuilderDisableInactiveUsersRunnerTests
    {
        private static readonly FakeClock SystemClock = new FakeClock { StubbedUtcNow = DateTimeOffset.UtcNow };
        private static readonly DisableInactiveUsersConfiguration Configuration = new DisableInactiveUsersConfiguration {InactiveTimeInDays = 1};
        private static readonly DateTime OutsideDisableWindow = SystemClock.UtcNow.Date.Subtract(new TimeSpan(Configuration.InactiveTimeInDays, 0, 1, 0));
        private static readonly DateTime InsideDisableWindow = SystemClock.UtcNow.Date.Subtract(new TimeSpan(Configuration.InactiveTimeInDays, 0, 0, 0));

        public async Task Should_Not_Disable_User_Who_Has_Logged_In_Recently()
        {
            // arrange
            var user = new User
            {
                ActiveDirectoryId = Guid.NewGuid(),
                Id = Guid.NewGuid(),
                DisplayName = "Test User",
                EmailAddress = $"{Guid.NewGuid()}-disable-test@example.com",
                UserStatus = UserStatus.Active,
                CreateDate = OutsideDisableWindow,
                LastLoginTime = InsideDisableWindow,
                Location = new Location
                {
                    FacilityName = $"{Guid.NewGuid()}-testlocation",
                }
            };

            // insert the test user into the database
            await InsertAsync(user);

            // act
            await ExecuteDbContextAsync(async (dbContext) =>
            {
                var sut = new DisableInactiveUsersRunner(dbContext, Configuration, SystemClock);
                await sut.Run();
            });

            // assert
            var updatedUser = await FindAsync<User>(user.Id);
            updatedUser.UserStatus.ShouldBe(UserStatus.Active);
        }

        public async Task Should_Disable_User_Who_Has_Not_Logged_In_Recently()
        {
            // arrange
            var user = new User
            {
                ActiveDirectoryId = Guid.NewGuid(),
                Id = Guid.NewGuid(),
                DisplayName = "Test User",
                EmailAddress = $"{Guid.NewGuid()}-disable-test@example.com",
                UserStatus = UserStatus.Active,
                CreateDate = OutsideDisableWindow,
                LastLoginTime = OutsideDisableWindow,
                Location = new Location
                {
                    FacilityName = $"{Guid.NewGuid()}-testlocation",
                    CcsFacilityCode = Guid.NewGuid().ToString()
                }
            };

            // insert the test user into the database
            await InsertAsync(user);

            // act
            await ExecuteDbContextAsync(async (dbContext) =>
            {
                var sut = new DisableInactiveUsersRunner(dbContext, Configuration, SystemClock);
                await sut.Run();
            });

            // assert
            var updatedUser = await FindAsync<ConnectUser>(user.Id);
            updatedUser.UserStatus.ShouldBe(UserStatus.DeactivatedForLackOfUserLogin);
        }
    }

Here we can see the twin perils awaiting us in the future: if we need to add new fields like a user’s first and last name we’ll have to make that change in two places. We’ll also have to read through multiple lines of set up and mentally compare the two to determine what a valid and invalid test case is.

Implement a simple builder

Let’s refactor this to use the builder pattern. The simplest builder is a single method that we can call to construct our test object in a common valid state.

Test with a builder
    class DisableInactiveUsersRunnerTests
    {
        private static readonly FakeClock SystemClock = new FakeClock { StubbedUtcNow = DateTimeOffset.UtcNow };
        private static readonly DisableInactiveUsersConfiguration Configuration = new DisableInactiveUsersConfiguration {InactiveTimeInDays = 1};
        private static readonly DateTime OutsideDisableWindow = SystemClock.UtcNow.Date.Subtract(new TimeSpan(Configuration.InactiveTimeInDays, 0, 0, 1));
        private static readonly DateTime InsideDisableWindow = SystemClock.UtcNow.Date.Subtract(new TimeSpan(Configuration.InactiveTimeInDays, 0, 0, 0));

        public async Task Should_Not_Disable_User_Who_Has_Logged_In_Recently()
        {
            // arrange
            var user = await InsertUser(OutsideDisableWindow, InsideDisableWindow);

            // act
            await RunDisableInactiveUsersRunner();

            // assert
            await VerifyUserStatusIs(user.Id, UserStatus.Active);
        }

        public async Task Should_Disable_User_Who_Has_Not_Logged_In_Recently()
        {

            // arrange
            var user = await InsertUser(OutsideDisableWindow, OutsideDisableWindow);

            // act
            await RunDisableInactiveUsersRunner();

            // assert
            await VerifyUserStatusIs(user.Id, UserStatus.DeactivatedForLackOfUserLogin);
        }

        // Four other tests not shown for brevity        

        private static async Task<User> InsertUser(DateTime createDate, DateTime? lastLoginTime)
        {
            var user = new User
            {
                ActiveDirectoryId = Guid.NewGuid(),
                Id = Guid.NewGuid(),
                DisplayName = "Test User",
                EmailAddress = $"{Guid.NewGuid()}-disable-test@example.com",
                UserStatus = UserStatus.Active,
                CreateDate = createDate,
                LastLoginTime = lastLoginTime,
                Location = new Location
                {
                    FacilityName = $"{Guid.NewGuid()}-testlocation",
                }
            };

            // insert the test user into the database
            await InsertAsync(user);
            return user;
        }

        private static async Task RunDisableInactiveUsersRunner()
        {
            await WithDisableInactiveUsers.ExecuteDbContextAsync(async (dbContext) =>
            {
                var sut = new DisableInactiveUsersRunner(dbContext, Configuration, SystemClock);
                await sut.Run();
            });
        }

        private async Task VerifyUserStatusIs(Guid userId, UserStatus expectedStatus)
        {
            var updatedUser = await WithDisableInactiveUsers.FindAsync<User>(userId);
            updatedUser.UserStatus.ShouldBe(expectedStatus);
        }
    }

Reuse builders across tests

Often we’ll need to create the same object in multiple test fixtures. This usually happens when a class is used in multiple features throughout the codebase. When this occurs, we can shift from a class method to creating a dedicated builder class that all of our tests can use.

As the domain complexity of our class increases, we can add methods to our builder to easily encapsulate the different states the objects can be in.

Shared builder class
public class UserBuilder
    {
        public User BuildEnabledUser()
        {
            var user = new User
            {
                ActiveDirectoryId = Guid.NewGuid(),
                Id = Guid.NewGuid(),
                DisplayName = "Test User",
                EmailAddress = $"{Guid.NewGuid()}-disable-test@example.com",
                UserStatus = UserStatus.Active,
                CreateDate = DateTime.Now.AddDays(-10),
                LastLoginTime = DateTime.Now.AddDays(-2),
                Location = new Location
                {
                    FacilityName = $"{Guid.NewGuid()}-testlocation",
                }
            };

            return user;
        }

        public User BuildDisabledUser()
        {
            var user = new User
            {
                ActiveDirectoryId = Guid.NewGuid(),
                Id = Guid.NewGuid(),
                DisplayName = "Test User",
                EmailAddress = $"{Guid.NewGuid()}-disable-test@example.com",
                UserStatus = UserStatus.DeactivatedForLackOfUserLogin,
                CreateDate = new DateTime(1999, 9, 9),
                LastLoginTime = new DateTime(2001, 7, 7),
                Location = new Location
                {
                    FacilityName = $"{Guid.NewGuid()}-testlocation",
                }
            };

            return user;
        }
    }

Expose states, not properties

Be careful not to make your construction methods too fine-grained, though. When a builder requires its caller to specify individual property values, it negates most of the benefit gained from using builders.

Too fine-grained
public async Task Should_Disable_User_Who_Has_Not_Logged_In_Recently()
{
		// While a "fluent" style is OK, this particular implementation forces developers
    // to set the individual properties without encapsulating the different states
    // like Enabled or Disabled that our user can be in
    var user = new UserBuilder()
    		.WithCreateDate(new DateTime(1999, 9, 9))
      	.WithLastLoginTime(new DateTime(2001, 7, 7))
      	.WithDisplayName("Test User");
}

Keep your builder count as low as possible

Generally, you’ll get the most value not by having one builder for every class, but by having builders that correspond with the aggregate roots in your business domain. For example, if you had an Order class and an OrderLineItem class, there’d be very little gain having separate OrderBuilder and OrderLineItem builders when both application and test code will be working at the order level anyway. Having a hierarchy of builders that shadow your classes means having to restructure two hierarchies, not just one, whenever the class associations change.

An exception to this is when a child object is used in several different features and has its own interesting state that’s beneficial to encapsulate. Sometimes for these scenarios, it makes sense to implement a mirroring composition of builders. One example of this in the medical domain could be a patient. You might have top-level concepts for a patient’s chart and a patient’s bill both relying on an underlying patient, so both PatientChartBuilder and PatientInvoiceBuilder might internally use a PatientBuilder.

Child builders
public class PatientBuilder
{
    public Patient BuildPatient(string mrn)
    {
        return new Patient
        {
            FirstName = "John",
            LastName = "Doe",
            MedicalRecordNumber = mrn
        };
    }
}

public class PatientChartBuilder
{
    public PatientChart BuildChart()
    {
        var patient = new PatientBuilder().BuildPatient("ABC");
        return new PatientChart()
        {
            Patient = patient,
            IcdCodes = new[] { "V95.43XA", "Y93.D" },
            RxScript = "Colace 100mg 1 tab PO qhs disp#30 refills#0"
        };
    }
}

public class PatientInvoiceBuilder
{
    public PatientInvoice BuildInvoice()
    {
        var patient = new PatientBuilder().BuildPatient("DEF");
        return new PatientInvoice()
        {
            Patient = patient,
            CptCodes = new[] { 99201, 10021 },
            InsuranceNumber = "XYZZY2012"
        };
    }
}

Only do this if your tests actually need these dependent objects in different states. We’ve seen how builders can be scaled up for complex needs, but you should start with the simplest thing that will work and only scale up when the simple approach isn’t working anymore.

Drive towards streamlined tests

Automated developer tests are useful because they help drive a clean design. They support verification today while providing a suite of regression checks for tomorrow’s changes. That value rapidly gets diluted, however, when you can’t quickly read and reason about a single test fixture is asserting. Test cases with slightly differing setups that blur into boilerplate mentally bogs you down— it becomes a game of “spot the difference”. You’ll also lose valuable time when a breaking change in how objects are created forces you to fix the many tests that duplicate the creation of what you’re adjusting. When you use builders to encapsulate common construction, you reduce repeated code into a single place and boost the readability within your tests, freeing you up to spend more time developing new features.

 

Resources
Design Patterns: Elements of Reusable Object-Oriented Software. 1994. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.

Let's Talk

Have a tech-oriented question? We'd love to talk.