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:
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:
- Sets up the
WebApplication
to access theHttpClient
and interact directly with the application's API. - Generates the scope required for accessing the
ServiceProvider
to manage database operations. - Constructs the test data.
- Persists the data into the database.
- 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.
Next article:
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.