AutoFixture – Make your tests slim and maintainable

Autofixture is an excellent library that can generate your tests data so as a developer you spent more time on what to test rather than how to setup data for tests. As you write less code to create the object you going to test so the code become more maintainable. For the example codes I am using following libraries:
1. XUnit – As unit testing framework
2. AutoFixture – A framework to generate test data
3. NSubstitute – A mocking library
4. Shouldly – A fluent assertion library.

Lets start with an example. I have a class as below:


public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    
    public string FullName
    {
        get { return string.Format("{0} {1}", FirstName, LastName); }
    }
}

Now I want to test the FullName property of person and Write the test as below:

public class Person_FullName_Should
{
    [Fact]
    public void Return_Based_On_FirstName_And_LastName()
    {
        var sut = new Person();
        sut.FirstName = "Mohammad Ruhul";
        sut.LastName = "Amin";

        var expectedFullName = string.Format("{0} {1}", sut.FirstName, sut.LastName);

        sut.FullName.ShouldBe(expectedFullName);
    }
}

Now according to the test above, the most important part is to make sure fullname generated based on firstname and lastname. If I don’t have to setup the person object then I end-up with less and more maintainable code. Lets see how Autofixture can help on this. In your test project add nuget package “AutoFixture.Xunit” and here is the Autofixture version of the above test.

public class Person_FullName_Should
{
    [Theory]
    [AutoData]
    public void Return_Based_On_FirstName_And_LastName(Person sut)
    {
        var expectedFullName = string.Format("{0} {1}", sut.FirstName, sut.LastName);

        sut.FullName.ShouldBe(expectedFullName);
    }
}

I hope the above example illustrate how it take care of the “Arrange” part of your tests. Now see how it makes tests more maintainable.

Lets see the following class that creates a new user.

UserService Class – Phase 1

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public Guid CreateUser(CreateUserInput input)
    {
        var newId = _userRepository.CreateUser(input);

        return newId;
    }
}

Now I need to write a test to ensure the service call the repository to create new user. We can write the test as below without autofixture:

public class UserService_CreateUser_Should
{
    [Fact]
    public void Call_Repository_To_CreateUser()
    {
        var input = new CreateUserInput
        {
            Email = "test@test.com",
            Password = "password"
        };
        
        var repository = Substitute.For<IUserRepository>();
        
        var sut = new UserService(repository); // This will break when another dependency introduced in userservice class
        
        sut.CreateUser(input);

        repository.Received().CreateUser(input);
    }
}

The test looks fine but there are two problems here. First problem is 50% of my code is for creating the UserService class. Which already explained in previous example and illustrated how AutoFixture can solve this problem. Other problem is maintainability.

Lets say now a new feature requested by client that when a new user created we need to send an email to the user account. From the requirement it is obvious that the existing test has no effect on this. But the reality is different. Let’s see what happen to the existing tests when we add new feature in user service class.

UserService Class – Phase 2

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    private readonly IEmailSender _emailSender;

    public UserService(IUserRepository userRepository, IEmailSender emailSender)
    {
        _userRepository = userRepository;
        _emailSender = emailSender;
    }

    public Guid CreateUser(CreateUserInput input)
    {
        var newId = _userRepository.CreateUser(input);

        _emailSender.SendWelcomeEmail(input.Email);

        return newId;
    }
}

As you see we didn’t change the code that call repository to save the user in database and Added a new dependency IEmailSender that send emails after account saved in database. Because of the new reference of IEmailSender, my existing test code now broken where I am creating new instance of UserService. So I have to fix that code as below.

public class UserService_CreateUser_Should
{
    [Fact]
    public void Call_Repository()
    {
        var input = new CreateUserInput
        {
            Email = "test@test.com",
            Password = "password"
        };
        var repository = Substitute.For<IUserRepository>();
        var emailSender = Substitute.For<IEmailSender>();
        var sut = new UserService(repository, emailSender);
        sut.CreateUser(input);

        repository.Received().CreateUser(input);
    }
}

This is just a start of pain of maintaining your tests. Imagine you later add ILogger in userservice to log and your tests will be broken again. AutoFixture can help us on this.

Lets start from beginning where I am writing my first test before adding new feature of sending email using AutoFixture and see how using AutoFixture saves me from having broken test just because I am adding new feature.

Add a nuget package reference to your test project “AutoFixture.AutoNSubstitute” and write the following attribute so that AutoFixture use NSubstitute to create mocked dependency.

public class AutoSubstituteDataAttribute : AutoDataAttribute
{
    public AutoSubstituteDataAttribute()
        : base(new Fixture().Customize(new Ploeh.AutoFixture.AutoNSubstitute.AutoConfiguredNSubstituteCustomization()))
    {
    }
}

Cool we are now ready to redo the process using AutoFixture. Reset the userservice class as it was in “Phase -1” and then write the test using AutoFixture as below.

public class UserService_CreateUser_Should
{
    [Theory]
    [AutoSubstituteData]
    public void Call_Repository([Frozen] IUserRepository userRepository,
        UserService sut,
        CreateUserInput input)
    {
        sut.CreateUser(input);

        userRepository.Received().CreateUser(input);
    }
}

Now implement email feature by adding a IEmailSender reference in UserService class as we did in Phase-2. Surprise! your existing test still passing and code is 50% less than non autofixture version.

AutoFixture creating the UserService class so the new refernece of IEmailSender is mocked by AutoFixture while creating an instance of userService class. Now write test for email functionality as below.

public class UserService_CreateUser_Should
{
    [Theory]
    [AutoSubstituteData]
    public void Call_Repository([Frozen] IUserRepository userRepository,
        UserService sut,
        CreateUserInput input)
    {
        sut.CreateUser(input);

        userRepository.Received().CreateUser(input);
    }
    
    [Theory]
    [AutoSubstituteData]
    public void Send_Email_To_New_Users_Email([Frozen] IEmailSender emailSender,
        UserService sut,
        CreateUserInput input)
    {
        sut.CreateUser(input);
        emailSender.Received().SendWelcomeEmail(input.Email);
    }
}

Just incase you wonder what the “Frozen” attribute mean. Frozen attribute tells autofixture to reuse same instance if required by other parameters. Otherwize our behaviour tests will not work.

Please note, this post not intended to show what to test and what not to test but to show how the use of AutoFixture can make your tests more maintainable.

There are similar libraries for AutoFixture that works with NUnit (AutoFixture.NUnit2) and Moq (AutoFixture.AutoMoq).

That’s all for today. Happy coding 🙂

Advertisements

2 thoughts on “AutoFixture – Make your tests slim and maintainable

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s