Using Respawn with Integration Testing

Integration Testing

In software testing, integration tests are used to test the interaction between different parts of the system, such as the interaction between the database and other APIs and services.

Observing the testing pyramid, integration tests are often found in the middle of the pyramid, making them more expensive than unit tests but less expensive than end-to-end tests. In my career, I have seen many projects more oriented towards integration testing because they bring more value to the table than just unit tests. While they can create some false positives and negatives, they are still very important to have in your test suite.

One of the common problems that integration tests face is the state of the database and how to manage it, their slow execution, and strategies to make them faster. In this blog post, I will show you how to use Respawn, a small utility created by Jimmy Board to help in resetting test databases to a clean state for integration tests.

The Elephant in the Room: The State of the Database

When writing your integration tests, it’s crucial to ensure the database state is consistent before each test. This consistency is vital because it guarantees that each test is independent of the others, and that the database state does not influence the test outcomes.

Achieving this is straightforward with a few integration tests where database creation takes only a few seconds, and setting up a database for each test is feasible. However, with a large number of tests, this process can become time-consuming and significantly slow down your test suite.

How Can We Address this Challenge?

One solution is to utilize Respawn, a NuGet package designed to reset the database state before each test. By using Respawn, you can ensure the database is in the same state before each test, affirming that each test is independent of others regarding the database state.

Let me guide you through setting up Respawn in your integration tests and demonstrate its usage.

Setting up Respawn in Your Integration Tests

I have created a simple API project featuring a Customer Entity and CustomerEndpoint (minimal API) that interacts with the database. Additionally, I have implemented a database layer with a repository pattern, which you might find in your typical project.

And let’s assume there’s a straightforward flow: from web client to API, then API to database.

Our aim is to test this flow with integration tests, ensuring the database state remains consistent before each test. I won’t delve into how I set up the API, however, I will demonstrate how to integrate Respawn in your integration tests.

When selecting a testing framework for your project, there are two popular opinions: XUnit and NUnit. Although both frameworks were developed by the same developers, XUnit is considered more modern, while many developers still view NUnit as the better option. For that reason, my preference is XUnit, so I will guide you through setting up Respawn with XUnit.

Setup Tests

First, you will need to create your XUnit test project. The common practice when naming test projects is to reflect what the test project represents. For example, if you were creating a unit test project, it might be named something like “MyProject.UnitTests.” In our case, since we are creating integration tests, we will name it “MyProject.IntegrationTests”

Once we’ve successfully created the XUnit project, we’ll need to add a few NuGet packages to our project:

  • Bogus: A popular library for generating fake data, which is incredibly useful for creating realistic test scenarios without the need to manually craft data sets.
  • FluentAssertions: A popular library for writing assertions in a more fluent, readable manner. It enhances test readability and the expressiveness of assertions.
  • Microsoft.AspNetCore.Mvc.Testing: This library is essential for testing ASP.NET Core applications. It allows for integration testing by simulating server behavior and making requests to the endpoints in a way that closely resembles a real client-server interaction.
  • Respawn: This is the library we’re focusing on for resetting the state of the database. It ensures that each test starts with a known state, which is critical for the reliability of integration tests.
    These libraries collectively provide a robust foundation for writing, executing, and managing integration tests in projects that utilize ASP.NET Core and interact with databases.

Here are the dotnet CLI commands to add these packages:

dotnet add package Bogus
dotnet add package FluentAssertions
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Respawn

With those libraries in place, it’s time to delve into our two integration tests.

Create Endpoint Tests

using Test_Respawn.DB;
using static Test_Respawn.IntegrationTests.SharedCollectionsNames;

namespace Test_Respawn.IntegrationTests.CustomerEndpoint;

[Collection(SHARED_API_COLLECTION)]
public class CreateEndpoint(CustomerAPIFactory apiFactory)
{
    private readonly HttpClient _httpClient = apiFactory.HttpClient;

    private static readonly Faker<Address> _addressFaker = new Faker<Address>()
        .RuleFor(x => x.City, f => f.Address.City())
        .RuleFor(x => x.PostalCode, f => f.Address.ZipCode());

    private static readonly Faker<Customer> _customerFaker = new Faker<Customer>()
        .RuleFor(x => x.FirstName, f => f.Person.FirstName)
        .RuleFor(x => x.LastName, f => f.Person.LastName)
        .RuleFor(x => x.Email, f => f.Person.Email)
        .RuleFor(x => x.PhoneNumber, f => f.Person.Phone)
        .RuleFor(x => x.Address, _ => _addressFaker.Generate());

    [Fact]
    public async Task Create_WhenValid_ShouldReturnId()
    {
        // Arrange
        var customer = _customerFaker.Generate();
        // Act
        var response = await _httpClient.PostAsJsonAsync("api/customer", customer);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}

Update Endpoint Tests

using Test_Respawn.DB;
using static Test_Respawn.IntegrationTests.SharedCollectionsNames;

namespace Test_Respawn.IntegrationTests.CustomerEndpoint;

[Collection(SHARED_API_COLLECTION)]
public class UpdateEndpoint(CustomerAPIFactory apiFactory)
{
    private readonly HttpClient _httpClient = apiFactory.HttpClient;

    private Customer _customer => new()
    {
        FirstName = "John",
        LastName = "Doe",
        Email = "johndoe@johndoe.com",
        PhoneNumber = "123456789",
    };

    [Fact]
    public async Task Update_FirstName_Only_ShouldNotUpdateOtherProperties()
    {
        // Arrange
        var customer = _customer;

        // Act
        await _httpClient.PostAsJsonAsync("api/customer", customer);

        // Intentionally bad code to demonstrate the problem
        var updateResponse = await _httpClient.PutAsJsonAsync("api/customer", new Customer
        {
            Id = 1,
            FirstName = "Jane"
        });

        var updatedCustomer = await updateResponse.Content.ReadFromJsonAsync<Customer>();

        // Assert
        updateResponse.StatusCode.Should().Be(HttpStatusCode.OK);
        updatedCustomer!.FirstName.Should().Be("Jane");
        updatedCustomer.Should().BeEquivalentTo(_customer, opt =>
        {
            opt.Excluding(e => e.Id);
            opt.Excluding(e => e.FirstName);
            return opt;
        });
    }
}

If I were to run these tests, one would succeed, but the other would fail. You might wonder why this happens. Let’s try to illustrate and understand the underlying reasons for this outcome. By examining the differences between the two tests and their interactions with the database

Now that we have a better understanding, it’s clear why the test ‘Create_WhenValid_ShouldReturnId’ results in the database being in a ‘dirty’ state. The database is not in a clean state after this test, and the test ‘Update_FirstName_Only_ShouldNotUpdateOtherProperties’ relies on the database being in a clean state for its execution.

This scenario illustrates why coupling tests together, which depend on the database’s state from a previous test, is a common mistake. It underscores the importance of ensuring each test is independent and starts with a known, clean database state to avoid unintended interactions and dependencies between tests.

Let’s fix this by configuring Respawn.

Respawn Configuration

using Respawn;
using Test_Respawn.API;

namespace Test_Respawn.IntegrationTests;

public class CustomerAPIFactory : WebApplicationFactory<IAPIMarker>, IAsyncLifetime
{
    private Respawner _respawner = default!;
    
    private const string _connectionString = "Server=localhost;Database=Test_Respawn;User Id=sa;Password=strong_password;TrustServerCertificate=true;";

    public HttpClient HttpClient { get; private set; } = default!;

    public async Task ResetDatabaseAsync() => await _respawner.ResetAsync(_connectionString);

    public async Task InitializeAsync()
    {
        HttpClient = CreateClient();
        _respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions
        {
            DbAdapter = DbAdapter.SqlServer,
            WithReseed = true,
            SchemasToInclude = ["dbo"]
        });
    }
    
    public new async Task DisposeAsync()
    {
        await ResetDatabaseAsync();
    }
}

Let me break down what’s happening here. The CustomerAPIFactory is a standard WebApplicationFactory that inherits from the IAsyncLifetime interface. This setup introduces two crucial methods: InitializeAsync and DisposeAsync. The IAsyncLifetime interface ensures that, for each test run, InitializeAsync is called before the test starts, setting up the necessary environment, and DisposeAsync is called after the test finishes to clean up resources.

By integrating IAsyncLifetime with Respawn, we achieve a mechanism where before each test starts, Respawn is used to reset the database to a clean state. This approach addresses the issue of tests affecting each other due to shared state in the database.

With Respawn ensuring a clean database before each test, and with adjustments made to the other two tests to accommodate this setup, all tests should now pass. This method guarantees that each test is independent, promoting more reliable and accurate test outcomes.

Conclusion

In conclusion, the integration of Respawn with our testing framework using the IAsyncLifetime interface in the CustomerAPIFactory demonstrates a powerful approach to ensuring the reliability and independence of integration tests. This methodology addresses common pitfalls associated with testing environments where a shared state can lead to unpredictable outcomes.

By leveraging Respawn to reset the database state before each test, we maintain a clean testing environment, ensuring that each test runs under consistent conditions. We also get a database reset process that is as fast as possible. This setup not only facilitates a more streamlined testing process but also significantly reduces the chances of tests failing due to dependencies on the database state set by previous tests.

If you’d like help implementing this strategy in your integration test, contact Trailhead about it, and we can help you get started.

Picture of Vladan Petrovic

Vladan Petrovic

Vladan earned his B.S in Computer at Faculty of Organisational Sciences of the University in Belgrade.Vladan is a result-oriented engineer with over 11 years of experience in the development of web and desktop applications. He has a deep knowledge of C#, .NET, and has a strong background in algorithms and data structures, mathematics, statistics, and data analysis. Vladan enjoys tackling complex tasks and is committed to staying up-to-date with the latest technologies and industry trends. Overall, his combination of technical expertise, experience, and passion for software development make him a valuable asset to the team. In his free time, he enjoys traveling to new and interesting places, basketball, photography, blogging, and hiking.

Free Consultation

Sign up for a FREE consultation with one of Trailhead's experts.

"*" indicates required fields

This field is for validation purposes and should be left unchanged.

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.