Getting started with Testcontainers for .NET
Testcontainers is a testing library that provides easy and lightweight APIs for bootstrapping integration tests with real services wrapped in Docker containers. Using Testcontainers, you can write tests that talk to the same type of services you use in production, without mocks or in-memory services.
If you are new to Testcontainers, please read What is Testcontainers, and why should you use it? to learn more about it. |
Let us look at how we can use Testcontainers to test a .NET application using a Postgres database.
Create a solution file with a source and test project
Create a .NET source and test projects from your terminal or favorite IDE.
$ dotnet new sln -o TestcontainersDemo
$ cd TestcontainersDemo
$ dotnet new classlib -o CustomerService
$ dotnet sln add ./CustomerService/CustomerService.csproj
$ dotnet new xunit -o CustomerService.Tests
$ dotnet sln add ./CustomerService.Tests/CustomerService.Tests.csproj
$ dotnet add ./CustomerService.Tests/CustomerService.Tests.csproj reference ./CustomerService/CustomerService.csproj
Once the projects are created, add the Npgsql dependency to the source project as follows:
dotnet add ./CustomerService/CustomerService.csproj package Npgsql
Now you should have the following dependencies in the source and test projects.
<PackageReference Include="Npgsql" Version="7.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"/>
We have added Npgsql, a Postgres ADO.NET Data Provider for talking to the Postgres database, and using xUnit for testing our service.
Implement business logic
We are going to create a CustomerService
class to manage customer details.
First, let us create a Customer
type as follows:
namespace Customers;
public readonly record struct Customer(long Id, string Name);
Create a DbConnectionProvider
class to hold ADO.NET connection parameters
(connection string) and create a method to get a database connection, as follows:
using System.Data.Common;
using Npgsql;
namespace Customers;
public sealed class DbConnectionProvider
{
private readonly string _connectionString;
public DbConnectionProvider(string connectionString)
{
_connectionString = connectionString;
}
public DbConnection GetConnection()
{
return new NpgsqlConnection(_connectionString);
}
}
Create a CustomerService
class and add the following code:
namespace Customers;
public sealed class CustomerService
{
private readonly DbConnectionProvider _dbConnectionProvider;
public CustomerService(DbConnectionProvider dbConnectionProvider)
{
_dbConnectionProvider = dbConnectionProvider;
CreateCustomersTable();
}
public IEnumerable<Customer> GetCustomers()
{
IList<Customer> customers = new List<Customer>();
using var connection = _dbConnectionProvider.GetConnection();
using var command = connection.CreateCommand();
command.CommandText = "SELECT id, name FROM customers";
command.Connection?.Open();
using var dataReader = command.ExecuteReader();
while (dataReader.Read())
{
var id = dataReader.GetInt64(0);
var name = dataReader.GetString(1);
customers.Add(new Customer(id, name));
}
return customers;
}
public void Create(Customer customer)
{
using var connection = _dbConnectionProvider.GetConnection();
using var command = connection.CreateCommand();
var id = command.CreateParameter();
id.ParameterName = "@id";
id.Value = customer.Id;
var name = command.CreateParameter();
name.ParameterName = "@name";
name.Value = customer.Name;
command.CommandText = "INSERT INTO customers (id, name) VALUES(@id, @name)";
command.Parameters.Add(id);
command.Parameters.Add(name);
command.Connection?.Open();
command.ExecuteNonQuery();
}
private void CreateCustomersTable()
{
using var connection = _dbConnectionProvider.GetConnection();
using var command = connection.CreateCommand();
command.CommandText = "CREATE TABLE IF NOT EXISTS customers (id BIGINT NOT NULL, name VARCHAR NOT NULL, PRIMARY KEY (id))";
command.Connection?.Open();
command.ExecuteNonQuery();
}
}
Let us understand what is going on in the CustomerService
class.
_dbConnectionProvider.GetConnection()
gets a database connection using ADO.NET.CreateCustomersTable()
method creates thecustomers
table if it does not already exist.GetCustomers()
method fetches all rows from thecustomers
table, populates data intoCustomer
objects, and returns a list ofCustomer
objects.Create(Customer)
method inserts a new customer record into the database.
Now let us see how we can test the CustomerService
logic using Testcontainers.
Add Testcontainers dependencies
Before writing Testcontainers-based tests, let’s add Testcontainers PostgreSql module dependency to the test project as follows:
dotnet add ./CustomerService.Tests/CustomerService.Tests.csproj package Testcontainers.PostgreSql
Now your test project should have Testcontainers.PostgreSql dependency added as follows:
<PackageReference Include="Testcontainers.PostgreSql" Version="3.3.0" />
As we are using a Postgres database for our application, we added the Testcontainers Postgres module as a test dependency.
Write test using Testcontainers
Create a CustomerServiceTest
class in the test project with the following
code:
using Testcontainers.PostgreSql;
namespace Customers.Tests;
public sealed class CustomerServiceTest : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:15-alpine")
.Build();
public Task InitializeAsync()
{
return _postgres.StartAsync();
}
public Task DisposeAsync()
{
return _postgres.DisposeAsync().AsTask();
}
[Fact]
public void ShouldReturnTwoCustomers()
{
// Given
var customerService = new CustomerService(new DbConnectionProvider(_postgres.GetConnectionString()));
// When
customerService.Create(new Customer(1, "George"));
customerService.Create(new Customer(2, "John"));
var customers = customerService.GetCustomers();
// Then
Assert.Equal(2, customers.Count());
}
}
Let us understand the code in our CustomerServiceTest
class.
Declare
PostgreSqlContainer
by passing the Docker image namepostgres:15-alpine
to the Postgres builder.The Postgres container is started using xUnit.net’s
IAsyncLifetime
interface, which executesInitializeAsync
immediately after the test class has been created.ShouldReturnTwoCustomers()
test initializesCustomerService
, insert two customer records into the database, fetch all the existing customers, and assert the number of customers.Finally, the Postgres container is disposed in the
DisposeAsync()
member, which gets executed after the test method is executed.
Now let’s run the tests as follows:
dotnet test
By running the customer service test, you can see in the output that Testcontainers pulled the Postgres Docker image from DockerHub if it’s not already available locally, started the container, and executed the test.
Voila! You have your first Testcontainers-based test running.
Conclusion
We have explored how to use Testcontainers for .NET library to test a .NET application using a Postgres database.
We have seen how writing an integration test using Testcontainers is very similar to writing a unit test that you can run from your IDE. Also, any of your teammates can clone the project and run tests without installing Postgres on their computers.
In addition to Postgres, Testcontainers provides dedicated modules for many commonly used SQL databases, NoSQL databases, messaging queues, etc. You can use Testcontainers to run any containerized dependency for your tests!
You can explore more about Testcontainers at https://www.testcontainers.com/.