DEV Community

Cover image for Easily Create Mock Data for Unit Tests
Dino Kacavenda
Dino Kacavenda

Posted on • Originally published at kaca.hashnode.dev

Easily Create Mock Data for Unit Tests

In this article, I will show you how to improve your unit tests with the use of the builder design pattern. I will use it to solve one of the hardest problems when writing unit tests: creating mock data. Mock data created using the builder pattern will:

  • be easy to read and understand
  • be reusable
  • ensure mock data doesn't break the domain rules

I will be using C# in the following example, but the principles can easily be applied to other languages.

Initial Solution

First I will show you a solution that doesn't use the builder pattern. For this example, we will be mocking a User object that has the following definition:

public class User
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Email { get; set; }
  public DateTime DateOfBirth { get; set; }
  public bool? IsDeleted { get; set; }
  public DateTime? DeletedOn { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Let's create two mock User objects, where one object has only required properties (Id, Name, DateOfBirth, and Email), and another that mocks a deleted User:

var user = new User
{
  Id = 1,
  Name = "John Locke",
  DateOfBirth = new DateTime(2000, 1, 1),
  Email = "johnlocke@gmail.com"
 };

var deletedUser = new User
{
  Id = 2,
  Name = "Tom Sawyer",
  DateOfBirth = new DateTime(2000, 1, 1),
  Email = "tomsawyer@gmail.com",
  // only this property really matters for this mock
  IsDeleted = true
};
Enter fullscreen mode Exit fullscreen mode

We see that with this approach we have to type a lot of code. Also, it is hard for the reader to notice what properties really matter for the specific test, and which are there because our object should always have them. We also need to be careful not to break specific domain rules (for example create 2 mock users with the same id).

If we added a new required property to the User object, we would need to add it to all of the mocks, otherwise, we might risk tests passing, when in fact, mock data is not valid. In the next section, we will create User mocks using the builder pattern and see how it improves the initial solution.

Builder Pattern

Builder pattern is a creational design pattern used for the easy creation of complex objects. Let's create a builder for the User object. We will start by creating a new class: MockUserBuilder (it is good to have consistent naming in your project. One that I recommend is Mock[entity name]Builder) and add a default constructor:

public class MockUserBuilder
{
    private static int _id = 1;

    private readonly User _user;
    public MockUserBuilder()
    {
        _user = new User
        {
            Id = _id,
            Name = $"John Locke {_id}",
            DateOfBirth = new DateTime(2000, 1, 1),
            Email = $"johnlocke{_id}@gmail.com"
        };

        _id++;
    }
}
Enter fullscreen mode Exit fullscreen mode

In the constructor, we create a User object that is filled with mock data. This object needs to fulfill all the domain rules, so make sure you defined all the properties that are required. Also, we have a private static _id property, so every User will have a unique id.

Next, we will add helper methods that will be used to define specific properties of a User:

public class MockUserBuilder
{
    // ... our constructor

    public MockUserBuilder WithName(string name)
    {
        _user.Name = name;
        return this;
    }

    public MockUserBuilder WithEmail(string email)
    {
        _user.Email = email;
        return this;
    }

    public MockUserBuilder WithIsDeleted(DateTime? deletedOn)
    {
        _user.IsDeleted = true;
        _user.DeletedOn = deletedOn ?? DateTime.UtcNow;
        return this;
    }

   // methods for other properties

    public User Build() => _user;
}
Enter fullscreen mode Exit fullscreen mode

All helper methods have a similar naming structure: With[PropertyName]. Every method is used to set a specific property, and it returns the MockUserBuilder so we can use the fluent syntax. This might seem like a lot of boilerplate code just to set some properties, but when you start writing your builders you don't need to add methods for every property immediately. I recommend you add only the properties you need for the specific tests you write, and gradually add the rest as you need them.

We can also group properties that are always set together. An example of this is the WithIsDeleted method. When a User is deleted, we always set both the IsDeleted flag and DeletedOn. By using the builder pattern we can guarantee that our mock data will always set these properties together, so we don't forget to set them in all places when manually creating mock objects (as in the example from the initial solution).

Let's use our builder to rewrite mocks from the initial solution:

var user = new MockUserBuilder()
    .Build();

var deletedUser = new MockUserBuilder()
    .WithName("Tom Sawyer")
    .WithEmail("tomsawyer@gmail.com")
    .WithIsDeleted()
    .Build();
Enter fullscreen mode Exit fullscreen mode

We see that this solution is much more readable. For the first mock user, we didn't have to set required properties because they are automatically set inside MockUserBuilder. We could have simplified the deleted mock user if the specific name and email didn't matter, so it would have been even less code. The builder will guarantee each created user has a unique Id.

This pattern especially shines when testing some edge cases, for example we need two users with the same name:

var user1 = new MockUserBuilder()
    .WithName("Tom Sawyer")
    .Build();

var user2 = new MockUserBuilder()
    .WithName("Tom Sawyer")
    .Build();
Enter fullscreen mode Exit fullscreen mode

Here it is very clear that we wanted mock objects with exactly the same name. If we didn't use the builder pattern we would need to specify all of the required properties and then it would be hard to notice the important ones.

Further Improvements

Builder from the previous section is very powerful by itself. If you want to further improve your builders, here are some additional tips that you can use.

Creating a Clone Method

Usually, when testing edge cases we create many mock objects that differentiate in a single property. If we need to set all of the properties they share, it is easy to miss what property was specific for a mock object. To solve this, we can create a clone method. This method will create a new builder from the existing one, and copy all the properties set on the initial entity. If you use an object-oriented language (like C#) you can put this method in an abstract class which will be inherited by the specific mock builders. One way to do a deep clone is to serialize and then deserialize the builder object (this way even non-primitive values will be correctly cloned).

public abstract class BaseMockBuilder<TBuilder, TEntity>
{
    // we need to add JsonProperty so the protected property is included
    [JsonProperty]
    protected TEntity entity { get; set; }

    public TBuilder CloneBuilder()
    {
        // serialize and then deserialize builder to create a deep clone of the builder
        // using Newtonsoft.Json
        var json = JsonConvert.SerializeObject(this);
        return JsonConvert.DeserializeObject<TBuilder>(json);
    }

    // we can even move Build to the base class now
    public TEntity Build() => entity;
}
Enter fullscreen mode Exit fullscreen mode

And then we change the MockUserBuilder so it inherits from the BaseMockBuilder:

public class MockUserBuilder : BaseMockBuilder<MockUserBuilder, User>
{
    private static int _id = 1;

    // this is now defined in the base class
    private readonly User _user;

    // same as before, just rename _user to entity
Enter fullscreen mode Exit fullscreen mode

Using a Fake Data Generator

In the previous example, we hard-coded values for the mock values. There is an excellent nuget package that can generate fake data that will resemble real data: Bogus. You can easily integrate Bogus, and use it inside the builder constructor for initial values.

Combining Builder Pattern with Object Mother Pattern

In our example, the builder always started creating mock data from the same blueprint (defined in the builder constructor). If we have several types of different object configurations, we can expand the builder to provide multiple initial configurations. Then we can use the defined methods for setting properties to customize these objects.

If we created a lot of deleted users, we can utilize the object mother pattern to create a separate blueprint for them:

private MockUserBuilder()
{
    entity = new User
    {
        Id = _id,
        Name = "John Locke",
        DateOfBirth = new DateTime(2000, 1, 1),
        Email = $"johnlocke{_id}@gmail.com"
    };

    _id++;
}

// new constructor for deleted user that uses the initial constructor
// and adds deleted properties
private MockUserBuilder(DateTime? deletedOn) : this()
{
    WithIsDeleted(deletedOn);
}

// method for creating our default builder
public static MockUserBuilder Create() => new MockUserBuilder();

// method for creating a deleted user builder
public static MockUserBuilder CreateDeletedUser(DateTime? deletedOn = null) => new MockUserBuilder(deletedOn);
Enter fullscreen mode Exit fullscreen mode

Now we can use our separate factory methods to easily create different kinds of users:

var user = MockUserBuilder
    .Create()
    .WithEmail("mother@gmail.com")
    .Build();

var deletedUser = MockUserBuilder
    .CreateDeletedUser()
    .WithName("Tom Sawyer")
    .WithEmail("tomsawyer@gmail.com")
    .Build();
Enter fullscreen mode Exit fullscreen mode

You can read more about this pattern here.

Conclusion

In this article, I showed you how to use the builder pattern to easily create mock data for your unit tests. Also, I provided you with ways to improve on the initial pattern. I hope you find this pattern useful and would like to hear in the comment section below if you are already familiar with it and which potential improvements have you made.

Top comments (0)