My favorite unit test tech stack for .Net

My favorite unit test tech stack for .Net

15 years ago, C# did not have a large variety of unit test frameworks and supporting libraries.

That has changed in the meantime. The community has grown considerably and so has the range of libraries and tools that can be used for unit tests. While Asp.Net Core is the absolute standard for a REST API, there is hardly any software project that uses the same setup for unit tests.

The choice of a unit test project setup is also often very much driven by developer preference and opinions. I have even seen developers insisting on a specific unit test framework at the customer site.

Even though no project has ever failed with the selection of the unit test framework, it does have an impact on the productivity, maintainability, and readability of the code. And of course, I also have my preferences, which I would like to describe here.

The test framework

Let's first discuss the unit test framework we use.

I started using XUnit during the early days of .Net Core. At that time there was no framework besides XUnit that ran on .Net Core. The MSTest framework was ported later, but I like the ability of XUnit to write simple data-driven tests. Also, XUnit provides an easy way to share context between tests with the fixtures. This also makes it easy to write database and integration tests. There would also be NUnit in the field, but I never like NUnit, since it is quite heavyweight compared to XUnit.

The mocking library

Mocking is also an important aspect of unit testing. I have used NSubstitute and Moq in the past. I like Moq a bit more since it is a bit more powerful about mocking protected members and most of the projects I joined used Moq, so it is quite popular.

But I don't use Moq alone, I always use it in combination with Moq.AutoMocker. AutoMocker creates an instance of a target class and automatically mocks all its dependencies. When I introduce a new constructor parameter in a service I want only touch the tests that test some specific aspects of a new dependency and not all tests. AutoMocker helps with this approach since it adds the ability to create an instance of a class and automatically mocks all dependencies.

Example from the Moq.AutoMocker documentation:

var mocker = new AutoMocker();
var car = mocker.CreateInstance<Car>();

car.DriveTrain.ShouldNotBeNull();
car.DriveTrain.ShouldImplement<IDriveTrain>();
Mock<IDriveTrain> mock = Mock.Get(car.DriveTrain);

The fake data generator

In a project with large entities, it is a big effort to provide the test data. In various projects we implemented so-called entity generators. Entity generators are simple and configurable classes to create entity instances.

public class AgendaItemBuilder : EntityBuilder<AgendaItem>
{
    private string _title;

    public AgendaItemBuilder WithTitle(string title)
    {
        _title = title;
        return this;
    }

    protected override async Task<AgendaItem> EnhanceEntityAsync(AgendaItem entity)
    {
        entity.Title = string.IsNullOrEmpty(_title) ? $"Title of {nameof(AgendaItem)} {unique}" : _title;

        return await Task.FromResult(entity);
    }
}

This can be used for following:

var builder = new AgendaItemBuilder()
    .WithTitle("My Title");
var entity = builder.Build();

The advantage of those builders is, that you have a shared class preparing the test data for a specific entity. But it is still a big effort to write and maintain them. Another drawback is that the test data contains static data which are always the same until it is specified otherwise.

A while ago I came across a library called Bogus. Bogus uses a random generator to generate test data. The test data is context-specific, e.g. addresses, names, phone numbers, etc. Bogus has a huge list of data generators. We can inherit from a Bogus faker and share its configuration.

public sealed class AgendaItemFaker : Faker<AgendaItemFaker>
{
    public AgendaItemFaker()
    {
        RuleFor(c => c.Name, f => f.Random.String(1, 10));
    }
}

A fake can be used like the following snippet:

var faker = new AgendaItemFaker();
var item = faker.Generate();

I don't use Bogus alone, I always use it in combination with AutoBogus. AutoFaker is a wrapper of the Bogus Faker class and provides conventions and integration into Moq which makes everything fit together.

Our fake looks like this now:

public sealed class AgendaItemFaker : Faker<AgendaItemFaker>
{
    public AgendaItemFaker()
    {
        Configure(builder =>
                {
                        builder.WithConventions();
                });
    }
}

With this, the effort to write and maintain my test data generators are reduced to a minimum.

Summary

Here we are, at the end of the article. Testing code should be as clean and well-maintained as our productive code. My tech stack has been built with the goal of reducing boilerplate code and the maintainability effort of the tests to a minimum.

I'm using the following libraries in combination:

What is your opinion? Do you use a different tech stack? Did I forget something?