paint-brush
A Beginner's Guide to Testcontainers in ASP.NET Coreby@devleader
158 reads

A Beginner's Guide to Testcontainers in ASP.NET Core

by Dev LeaderFebruary 25th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Discover how Testcontainers revolutionizes ASP.NET Core testing by simplifying the setup of containerized testing environments. Learn how to integrate Testcontainers into your development pipeline, leverage parallel testing for increased efficiency, and ensure consistent testing across multiple stages with continuous integration and deployment.
featured image - A Beginner's Guide to Testcontainers in ASP.NET Core
Dev Leader HackerNoon profile picture


Testcontainers in ASP.NET Core provides a handy way to get our (potentially) complex dependencies set up to be tested against. It allows dotnet developers to easily spin up small, disposable containers to simulate dependencies required in testing where we might otherwise just stick to mocking out in unit tests. Testcontainers makes it easy to manage dependencies and eliminate the need for mock objects, giving us the flexibility to write different kinds of tests for our applications.


Using Testcontainers in ASP.NET Core testing is an important step towards achieving a more efficient development pipeline — and with better test coverage! By automating test dependencies, you can reduce the overall testing time, leading to faster and more efficient development processes. The purpose of this article is to provide you with some insights about Testcontainers in ASP.NET Core tests.


Let’s dive into it and see how we can write better tests!


Overview of ASP.NET Core Testing

When it comes to software development, you cannot neglect testing. It’s such a key part for us to be able to deliver high-quality software and even have the confidence to iterate on our changes. In ASP.NET Core, there are a bunch of ways that we can test. I’ve written about using xUnit to test ASP.NET applications and using NUnit for the same thing. However, testing with ASP.NET Core can also pose a few challenges, such as managing dependencies and ensuring tests run consistently.


To help overcome these challenges, we can use Testcontainers! This is an awesome tool that allows for containerized testing of applications and the associated dependencies. Containers provide a lightweight environment for testing with the dependencies of your code which can help ensure tests are run consistently across different environments. After all, getting these dependencies up and running once can be tricky… but doing it in a repeatable fashion across different systems and test runs is a whole different world of “fun”.


Testcontainers in ASP.NET Core can help streamline testing by allowing us to create these repeatable and consistent testing environments. This can be particularly useful in ensuring integration tests work as expected by providing the same environment on each run. By using Testcontainers, we can also reduce the possibility of discrepancies between tests by working with dependencies in the same containerized environments every time.


Getting Started with Testcontainers in ASP.NET Core

To get started with Testcontainers in ASP.NET Core, you’ll need to begin with installation and configuration. Here’s a brief guide on how to set up Testcontainers in your ASP.NET Core project:

Installation of Testcontainers in ASP.NET Core

The first step is to install Testcontainers into your project. This can be done using your project’s package manager, such as NuGet. Simply search for “Testcontainers” and install the appropriate package. Testcontainers is built on top of the popular Docker platform, so you’ll also need to have Docker installed on your machine before starting.


Depending on which dependencies you need for your tests (i.e. are you working with MySQL, MongoDB, Postgres, SQLite, etc…), you will need to pull in other dependencies. I cannot provide an exhaustive list in this single article, but I can at least call this to your attention so that you can pull in the other necessary pieces for your use case!

Basic Configuration and Setup

Once you’ve installed Testcontainers and you’ve got any additional packages pulled in for dependencies you’ll need, we need to ensure that we create the dependencies in code. We’ll go through an example of this later in the article to see how it looks.


Depending on which testing framework you’re using, you’ll want to consider things like:

  • When should I spin up my dependencies?
  • Can I and should I share dependencies as containers across tests? Is there a risk of shared mutable state?
  • When should I clean up my dependencies?


We’ll see this in more detail in the upcoming sections.

Creating and Starting Containers

We call Testcontainers from code to create and start our containers. This can be done easily using the Testcontainers API simply by creating a new container object, configuring it, and then starting the container. Once the container is running, you can use it for your tests as needed. The APIs available to us in Testcontainers often provide an easy mechanism to know which ports we can talk to on the container to access the services that are running inside the container.


Overall, getting started with Testcontainers in ASP.NET Core is relatively straightforward, and gets us that consistent testing environment for our applications. Getting setup and configuration code for our dependencies organized in a common spot allows us to quickly begin using Testcontainers in ASP.NET Core tests that we want to write.


If you’re interested in learning more about testing with Testcontainers, check out my affiliate link to this course on Dometrain:
[Affiliate] From Zero to Hero: Integration testing in ASP.NET Core - Dometrain


Testing with Testcontainers in ASP.NET Core

When writing tests with Testcontainers in ASP.NET Core, there are a few things to keep in mind. First, it’s important to understand how to create and manage your containerized testing environments using Testcontainers. Fortunately, this is relatively easy to do using the Testcontainers API.


Once you have your testing environment set up, you can begin writing tests using Testcontainers. When you’re writing tests with Testcontainers, there are a few different things to consider. For example, you can simulate dependencies between your code and external services using Testcontainers. This can be particularly useful for testing your application’s integrations with other services, such as databases or API endpoints.


To give you an idea of how to write tests with Testcontainers, we’ll go through this (hopefully) simple test using Testcontainers!

How to Write a Simple Test Using Testcontainers

To write a simple test using Testcontainers, you’ll first need to create a container object. In this example, we’ll assume you’re testing an ASP.NET Core application that requires a PostgreSQL database. First, create a new container for the PostgreSQL database using the Testcontainers API:

using System;
using DotNet.Testcontainers.Containers;
using Testcontainers.PostgreSql;

var container = new PostgreSqlBuilder()
    .WithDatabase("myTestDatabase")
    .WithUsername("testUser")
    .WithPassword("testPassword")
    .WithExposedPort(5432)
    .Build();


Once you’ve created your container object, you can start it and use it in your tests. Here’s an example of a simple test that uses the PostgreSQL container from an xUnit Test:

using DotNet.Testcontainers.Containers;

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;

using Testcontainers.PostgreSql;

using Xunit;

//
// This code goes into your main web app project
//
public sealed class YourWebApp
{
    // ... assume this is a type from your web application
    // that you want to be testing
}

public class MyDbContext(DbContextOptions options) :
    DbContext(options)
{
    public DbSet<Item> Items => Set<Item>();
}

public sealed record Item
{
    public int Id { get; set; }
    public string Name { get; set; }
}

//
// This code goes into your test project
//
public sealed class MyWebAppTests
{
    [Fact]
    public async Task TestAddingItemToDatabase()
    {
        // Arrange
        var container = await StartContainerAsync();
        await SeedTestDataAsync(container);

        // create a new web application factory to get an HTTP client
        // that can talk to our app!
        var webApplicationFactory = new WebApplicationFactory<YourWebApp>();
        var client = webApplicationFactory.CreateClient();

        // Act
        // TODO: perform your test action
        
        // Assert
        // TODO: assert the results
    }

    private async Task<IDatabaseContainer> StartContainerAsync()
    {
        // Create a new container
        var container = new PostgreSqlBuilder()
            .WithDatabase("myTestDatabase")
            .WithUsername("testUser")
            .WithPassword("testPassword")
            .WithExposedPort(5432)
            .Build();

        // Start the container
        await container.StartAsync();

        return container;
    }

    private MyDbContext OpenDbContext(IDatabaseContainer container)
    {
        // Connect to the database
        var connectionString = container.GetConnectionString();
        var options = new DbContextOptionsBuilder<MyDbContext>()
            .UseNpgsql(connectionString)
            .Options;

        return new MyDbContext(options);
    }

    private async Task SeedTestDataAsync(IDatabaseContainer container)
    {
        // Ensure the database is created
        using var dbContext = OpenDbContext(container);
        await dbContext.Database.EnsureCreatedAsync();

        // Add an item to the database
        var newItem = new Item
        {
            Id = 1,
            Name = "Test Item"
        };

        dbContext.Items.Add(newItem);
        await dbContext.SaveChangesAsync();
    }
}

In this example, we’re using Testcontainers to create a container for a PostgreSQL database, and then using that container to run a test that adds an item to the database. We take care of all of that inside the “Arrange” portion of our test setup. But it’s also important to note that because we’re dealing with a web application that we want to test, we can take advantage of using a WebApplicationFactory to be able to hit the APIs of our ASP.NET Core app.


For completeness, I also included some example code at the top of the snippet just so you can see a dummy DB context as well as an entity type that we can play around with. Of course, your application will look different, but I wanted the code example to highlight the necessary pieces to minimize confusion about where things are coming from.


Testing in Parallel with Testcontainers

When it comes to testing our code, one approach we can take to get better performance is to test in parallel. With Testcontainers in ASP.NET Core, we can greatly increase the efficiency and speed of our testing process if we are not spinning up and tearing down resources non-stop. It’s an additional layer of complexity to consider, but if we can do it properly, we can save a significant amount of time. In this section, I’ll cover the advantages of testing in parallel and show how Testcontainers can help with parallel testing.

Benefits of Testing in Parallel

Testing in parallel has several advantages. First, it can greatly reduce the amount of time it takes to run tests. Rather than running tests one at a time, tests can be run simultaneously in multiple containers, greatly reducing overall testing time. When we only have a couple of tests, sitting back and waiting a minute for them to run after spinning up and tearing down containers may not feel so bad. But if this time continues to increase as we add tests, this starts to become a huge burden.


Testing in parallel can help increase efficiency by allowing for quicker feedback on code changes. If we can run tests more quickly, we’ll be able to diagnose and fix issues more quickly, leading to faster development cycles. Ultimately, we’re after high confidence in our changes and getting that feedback as quickly (and as accurately/consistently) as we can.


Finally, testing in parallel can help with scalability. As an application grows and tests become more complex, running tests in parallel can help ensure that tests continue to run effectively and efficiently. I hinted at this already, but this may not be a problem for you currently… But when you reach a point where you don’t want to add more coverage because you don’t want to wait, it sort of feels like it’s too late.

Introduction to Parallel Testing with Testcontainers

To run tests in parallel with Testcontainers, we can make use of the parallelization functionality provided by popular test frameworks. xUnit is one of my preferred testing frameworks, so I’ll be speaking about it here… But there’s no reason these concepts cannot be applicable to other frameworks like NUnit and beyond.


In Xunit, tests can be run in parallel by using the Collection attribute to group related tests together, and then using a test runner like xunit.console to run tests in parallel across collections. To run tests in parallel with Testcontainers specifically, we can use an ASP.NET Core IAsyncLifetime interface to manage container lifecycle on a per-test basis. This allows tests to be run concurrently in different containers while still maintaining separate database and service contexts.

Implementation and Configuration of Parallel Testing

To configure parallel testing with Testcontainers, you should first choose a test runner and test framework that supports parallelization. If you’re working solo, pick whichever one you’re comfortable with. If you’re working on a team, either use what’s in place already or sit down with the team to make a decision together as a group.


Next, you’ll need to set up a Testcontainers management system that allows for parallel creation and management of test containers. If you’re working with xUnit like me, one option for this is to use the IAsyncLifetime interface to manage the creation and lifecycle of containers. However, a lot of the time we have the flexibility to pick different ways to do this. We just need to consider what hook points we have in our tests to set things up and tear them down. If you understand the lifecycle of your tests, then you can make decisions about where to tap into them.


You should also make sure that their code is properly architected to support parallelization — which is an easy one for us to gloss over here. I can’t speak to how you’ve written your application or your tests… so this might involve refactoring code into smaller, more modular components that can be tested in parallel if they don’t work that way already. You may want to lean on dependency injection to ensure that test objects are created independently of one another and manage lifetimes of things this way to stay better organized.


Using Testcontainers in Continuous Integration and Deployment

Continuous Integration and Continuous Deployment (CI/CD) pipelines are essential for maintaining a streamlined development process. By incorporating Testcontainers into your CI/CD pipeline, you can ensure that your application is fully tested in an environment consistent with production before any code is deployed. Testcontainers can also help automate the creation and management of test environments, making the process more efficient and effective.


One of the really cool things about using Testcontainers locally and in our CI/CD environments is that we can largely set up our dependencies the exact same way. Having things run on-box vs on-server for your tests and ensuring they’re as identical as possible is of incredible value. Imagine trying to debug issues in your tests but the issue only happens locally (or on your CI/CD server) but not the other way around. With Testcontainers, we can generally get all of our dependencies spun up the same way which is a huge benefit.

Testcontainers is compatible with many popular CI/CD tools and platforms, including Jenkins, GitLab, and Travis CI. By integrating Testcontainers into your pipeline, you can ensure that your code is tested consistently across multiple stages in the development process by leveraging container technology to create and manage testing environments.


You’ll also want to consider how to establish best practices for utilizing the CI/CD technology effectively with Testcontainers. These best practices may include creating separate container instances for each test job, utilizing container caching to speed up tests, and automating the management of test environments using infrastructure as code. Again, all things that need to be considered as you roll this sort of thing out for usage.


Wrapping Up Testcontainers in ASP.NET Core

Using Testcontainers in ASP.NET Core offers several benefits and advantages when it comes to building tests that give you confidence. It allows you to easily spin up and tear down testing environments in a matter of seconds, reducing the overall time and effort required for testing. I truly can’t remember how difficult and complex it used to be to try and set up environments for this sort of thing prior to having something like Testcontainers at my fingertips. Entire testing scenarios used to just go missing, or we’d be investing heavily into maintaining more complex testbed.


Testing is a critical part of software development — if you care about quality — and using Testcontainers can greatly streamline the process. Supporting more complex end-to-end style functional, integration, and system-level tests allows you to get a great deal of confidence. By reducing the risk of bugs and errors, you can build trust in your software and instill confidence in your users.


I encourage you to try out Testcontainers in your ASP.NET Core tests! You don’t need to ditch your unit tests, but you can layer these in! If you found this useful and you’re looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube!


Also published here.