In-Memory vs. Testcontainers
Mind Map Summary
- Topic: In-Memory vs. Testcontainers
- Core Concepts:
- In-Memory Provider: An EF Core database provider that stores data in memory. It is not a relational database and does not support all the features of a real database.
- Testcontainers: A library that allows you to run a real database in a Docker container for integration testing.
- Trade-offs:
- Speed:
- In-Memory: Faster, as it does not require a separate database server.
- Testcontainers: Slower, as it needs to start a Docker container.
- Fidelity:
- In-Memory: Low fidelity. It does not behave like a real relational database.
- Testcontainers: High fidelity. You are testing against the same database that you use in production.
- Complexity:
- In-Memory: Simple to set up and use.
- Testcontainers: More complex to set up, as it requires Docker.
- Speed:
Practice Exercise
Write two sets of integration tests for a repository. One set should use the EF Core in-memory database. The second set should use Testcontainers to spin up a real PostgreSQL or SQL Server container for the tests. Discuss any tests that pass with one setup but fail with the other.
Answer
1. In-Memory Tests:
using Microsoft.EntityFrameworkCore;
using Xunit;
public class ProductRepositoryInMemoryTests
{
[Fact]
public void Add_Product_Should_Save_To_Database()
{
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseInMemoryDatabase(databaseName: "TestDb")
.Options;
using (var context = new MyDbContext(options))
{
var repository = new ProductRepository(context);
repository.Add(new Product { Name = "Test Product" });
}
using (var context = new MyDbContext(options))
{
Assert.Equal(1, context.Products.Count());
Assert.Equal("Test Product", context.Products.Single().Name);
}
}
}
2. Testcontainers Tests:
using Microsoft.EntityFrameworkCore;
using Testcontainers.PostgreSql;
using Xunit;
public class ProductRepositoryTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder().Build();
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
}
public async Task DisposeAsync()
{
await _dbContainer.StopAsync();
}
[Fact]
public void Add_Product_Should_Save_To_Database()
{
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseNpgsql(_dbContainer.GetConnectionString())
.Options;
using (var context = new MyDbContext(options))
{
context.Database.EnsureCreated();
var repository = new ProductRepository(context);
repository.Add(new Product { Name = "Test Product" });
}
using (var context = new MyDbContext(options))
{
Assert.Equal(1, context.Products.Count());
Assert.Equal("Test Product", context.Products.Single().Name);
}
}
}
Discussion:
- A test that uses a database-specific feature, such as a raw SQL query with provider-specific syntax, would pass with Testcontainers but fail with the in-memory provider.
- A test that relies on case-sensitive comparisons might pass with the in-memory provider but fail with a case-insensitive database like SQL Server.
- In general, Testcontainers provides a much higher level of confidence that your code will work correctly in production, as you are testing against the same database that you use in production.