Integration tests are important for ensuring that your application components work together as expected. When it comes to testing APIs, having a reliable and isolated database environment can significantly improve the accuracy of your tests. By using test containers over other concepts like in memory database, you will actually test the behavior end-to-end, including the database behaviors. We will accomplish this using TestContainers to set up SQL Server for integration testing a Minimal API.
Prerequisites
- .NET 6 or later installed on your development machine.
- Docker installed and running.
- TestContainers for .NET library.
Setting Up the Project
First, create a new Minimal API project if you don’t have one already:
dotnet new web -n MinimalApiWithTestContainers
cd MinimalApiWithTestContainers
Add the necessary NuGet packages:
dotnet add package DotNet.Testcontainers
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Moq
Configuring the Minimal API
In this example service, we will work with products that we’re selling. Create a simple Product model and ProductDbContext for Entity Framework Core.
Models/Product.cs
namespace MinimalApiWithTestContainers.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
Data/ProductDbContext.cs
using Microsoft.EntityFrameworkCore;
using MinimalApiWithTestContainers.Models;
namespace MinimalApiWithTestContainers.Data
{
public class ProductDbContext : DbContext
{
public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
}
}
This code configures the API to use the ProductDbContext.
Program.cs
using Microsoft.EntityFrameworkCore;
using MinimalApiWithTestContainers.Data;
using MinimalApiWithTestContainers.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddDbContext<ProductDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Map endpoints
app.MapGet("/products", async (ProductDbContext db) => await db.Products.ToListAsync());
app.MapPost("/products", async (ProductDbContext db, Product product) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/products/{product.Id}", product);
});
app.Run();
Add the database connection string in the project’s appsettings.json.
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ProductsDb;Trusted_Connection=True;"
}
}
Setting Up Integration Tests with TestContainers
Now, let’s create a new test project for our integration tests using xUnit.
dotnet new xunit -n MinimalApiWithTestContainers.Tests
cd MinimalApiWithTestContainers.Tests
dotnet add reference ../MinimalApiWithTestContainers/MinimalApiWithTestContainers.csproj
Next, add the necessary NuGet packages for the test project.
dotnet add package DotNet.Testcontainers
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
And, create a test class to set up and use TestContainers with SQL Server.
IntegrationTests/ApiIntegrationTests.cs
using System.Net.Http.Json;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MinimalApiWithTestContainers;
using MinimalApiWithTestContainers.Models;
using Xunit;
namespace MinimalApiWithTestContainers.Tests.IntegrationTests
{
public class ApiIntegrationTests : IAsyncLifetime
{
private readonly TestcontainersContainer _sqlServerContainer;
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public ApiIntegrationTests()
{
_sqlServerContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("mcr.microsoft.com/mssql/server:2019-latest")
.WithEnvironment("ACCEPT_EULA", "Y")
.WithEnvironment("SA_PASSWORD", "Your_password123")
.WithPortBinding(1433)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433))
.Build();
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ProductDbContext>));
services.Remove(descriptor);
services.AddDbContext<ProductDbContext>(options =>
{
options.UseSqlServer($"Server=localhost,{_sqlServerContainer.GetMappedPublicPort(1433)};User Id=sa;Password=Your_password123;Database=ProductsDb;");
});
});
});
_client = _factory.CreateClient();
}
public async Task InitializeAsync()
{
await _sqlServerContainer.StartAsync();
}
public async Task DisposeAsync()
{
await _sqlServerContainer.StopAsync();
_sqlServerContainer.Dispose();
}
[Fact]
public async Task Get_Products_Returns_Empty_List()
{
var response = await _client.GetFromJsonAsync<List<Product>>("/products");
Assert.Empty(response);
}
[Fact]
public async Task Post_Product_Creates_Product()
{
var newProduct = new Product { Name = "Test Product", Price = 10.99m };
var response = await _client.PostAsJsonAsync("/products", newProduct);
response.EnsureSuccessStatusCode();
var product = await response.Content.ReadFromJsonAsync<Product>();
Assert.NotNull(product);
Assert.Equal("Test Product", product.Name);
Assert.Equal(10.99m, product.Price);
var products = await _client.GetFromJsonAsync<List<Product>>("/products");
Assert.Single(products);
}
}
}
Explanation
- TestcontainersBuilder: Configures the SQL Server container.
- WebApplicationFactory: Creates a test server for the API.
- ConfigureServices: Replaces the
ProductDbContextto use the SQL Server TestContainer. - InitializeAsync: Starts the SQL Server container.
- DisposeAsync: Stops and disposes of the SQL Server container.
- Test Methods: Tests for getting and posting products to the API.
Running the Tests
We can run the tests using the .NET CLI:
dotnet test
This setup ensures that your integration tests run against a real SQL Server instance in a Docker container, providing a reliable and isolated environment for each test run. These tests can be run locally, and in your CI/CD pipeline to ensure that your code is always well tested.
Conclusion
Using TestContainers for SQL Server in your integration tests for Minimal API ensures that your tests are accurate, isolated, and repeatable. This approach minimizes the risk of interference from other tests or external factors and greatly enhances your integration testing strategy.