Enhance your .NET Testing #3: Fixtures separation

Pierre Belin
Pierre Belin
Enhance your .NET Testing #3: Fixtures separation
Table of Contents
Table of Contents

In the previous article, we delved into a critical issue that arises when handling growing complexities in test data creation. As test data becomes more elaborate, the readability of tests suffers, and they become vulnerable to maintenance challenges. To combat this, it is imperative to emphasize test maintainability to prevent tests from being discarded or relegated to mere comments during iterative development.

As a sequel to our previous discussion on unit testing in .NET Core, this article aims to delve deeper into these tools. If you haven't read it, I recommand you to start by reading it:

Elevate Your .NET Testing Strategy #2: InMemoryDatabase
The development environment in C#/.NET offers a myriad of tools that optimize testing efficiency. Among these are the InMemoryDatabase and WebApplicationFactory, both possessing the potential to drastically streamline testing strategies and development processes. As a sequel to our previous discuss…

This article introduces a practical technique to optimize test data creation by encapsulating the entire logic within a separate fixture class.

The Dilemma of the Catch-All Test:

Revisiting our earlier example:

public async Task ShouldGetGoatsList_WhenAskingToRouteGetAllGoats()
{
    // Arrange
    var webApplication = new GoatWebApplication();
    var serviceProvider = webApplication.Services.CreateScope().ServiceProvider;

    var goat = new Goat() { Id = 1, Name = "John" };
    var goatRepository = serviceProvider.GetService<IGoatRepository>().Add(goat);
    var unitOfWork = serviceProvider.GetService<IUnitOfWork>().SaveChangesAsync();
        
    // Act
    var response = await webApplication.CreateClient().GetAsync("PATHTOGETALLGOATS");

    // Arrange
    var content = await response.Content.ReadAsStringAsync();
    response.StatusCode.Should().Be(HttpStatusCode.OK, content);
    var sites = JsonSerializer.Deserialize<List<Site>>(content);
    sites.Should().HaveCount(1);
}

This single test performs multiple tasks:

  1. Sets up the WebApplication to access the HttpClient and interact directly with the application's API.
  2. Generates the scope required for accessing the ServiceProvider to manage database operations.
  3. Constructs the test data.
  4. Persists the data into the database.
  5. Executes the test's specific tasks.

The objective is to streamline the first four steps, improving test readability and clarity.

When examining the test code, it becomes evident that access to the UnitOfWork or the ServiceProvider may not be necessary for this particular scenario. The inclusion of these elements introduces unnecessary verbosity and could potentially perplex future developers, questioning the intent behind these lines. While this specific example may seem straightforward, it is essential to acknowledge that similar situations can get more complex in larger projects.

Hence, the primary question to address is: Does the approach of creating test data add substantial value to this test?

The answer is a resounding no. Irrespective of the intricacies involved in the test data creation process, what truly matters is understanding which data is being added and verifying whether it produces the expected test outcomes.

Embrace Fixture Separation

To tackle these concerns effectively, we advocate for the utilization of the GoatFixture class, which serves as a specialized container for methods responsible for service interactions.

internal class GoatFixture 
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly  IGoatRepository _goatRepository;
    public HttpClient Client { get; }
    private readonly  IServiceProvider _serviceProvider;
    
    public GoatFixture()
    {
        var webApplication = new GoatWebApplication();
        var serviceScope = webApplication.Services.CreateScope();

        Client = webApplication.CreateClient();

        _serviceProvider = serviceScope.ServiceProvider;
        _goatRepository = ServiceProvider.GetService<IGoatRepository>() ?? throw new Exception();
        _unitOfWork = ServiceProvider.GetService<IUnitOfWork>() ?? throw new Exception();
    }

    public CustomersFixture AddGoat(int id, string name)
    {
        var goat = new Goat { Id = id, Name = name };
        IGoatRepository.Add(customer);
        return this;
    }

    public CustomersFixture SaveInDatabase()
    {
        _unitOfWork.SaveChangesAsync(default);
        return this;
    }
}

By exposing only the HttpClient, tests gain access to the client for HTTP requests while abstracting away the complexities of data setup.

This dedicated class encapsulates methods for adding new entities to the database and ensuring proper data persistence once the dataset is created.

With the newfound capabilities offered by the fixture class, the previous code for creating test data undergoes a remarkable transformation:

// New version
 var webApplication = _fixture.AddGoat(1, "John").SaveInDatabase();
 
// Old version
var webApplication = new GoatWebApplication();
var serviceProvider = webApplication.Services.CreateScope().ServiceProvider;

var goat = new Goat() { Id = 1, Name = "John" };
var goatRepository = serviceProvider.GetService<IGoatRepository>().Add(goat);
var unitOfWork = serviceProvider.GetService<IUnitOfWork>().SaveChangesAsync();

The difference is striking! Clarity and simplicity are now the highlights, allowing developers to focus on the essential aspects of the test.

Effectively implementing the GoatFixture class requires a straightforward instantiation for each test. In the context of xUnit, declaring it as a property ensures automatic utilization.

public class GoatTests 
{
    private readonly GoatFixture _goatFixture = new GoatFixture();
}

And here's the final result !

public async Task ShouldGetGoatsList_WhenAskingToRouteGetAllGoats()
{
    // Arrange
    var webApplication = _fixture.AddGoat(1, "John").SaveInDatabase();
            
    // Act
    var response = await webApplication.CreateClient().GetAsync("PATHTOGETALLGOATS");

    // Arrange
    var content = await response.Content.ReadAsStringAsync();
    response.StatusCode.Should().Be(HttpStatusCode.OK, content);
    var sites = JsonSerializer.Deserialize<List<Site>>(content);
    sites.Should().HaveCount(1);
}

Summary

The adoption of fixture separation and the utilization of the dedicated GoatFixture class present developers with a powerful solution to streamline test setup and elevate code consistency in Visual Studio.

By consolidating test data creation logic, this approach enhances test readability, maintainability, and overall efficiency, promoting collaboration and productivity within the development process.

Embracing these techniques empowers developers to unlock the full potential of testing, ensuring reliable and successful software outcomes.

Have a goat day 🐐

Most of the article's content comes from Rémi Henache's work on one of our customer projects.



Join the conversation.

Great! Check your inbox and click the link
Great! Next, complete checkout for full access to Goat Review
Welcome back! You've successfully signed in
You've successfully subscribed to Goat Review
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated