// Testing · 06
Integration Tests with Real Dependencies
Run the code against a real database, queue and web host.
How it fits together
- Test host uses / calls Orders API (in-process) (1 POST /orders)
- Orders API (in-process) uses / calls Database container (2 INSERT order)
- Database container fails / error path Orders API (in-process) (3 unique clash)
- Orders API (in-process) fails / error path Test host (4 409 Conflict)
Spot it
- Tests pass but a production query fails
- A mocked repository hides a SQL error
- Behaviour differs between test and prod databases
It costs
Slower to start and needs Docker, but it catches what doubles hide.
In code
public sealed class PostgresFixture : IAsyncLifetime{ private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() .WithImage("postgres:16") .Build(); public string ConnectionString => _container.GetConnectionString(); public async Task InitializeAsync() { await _container.StartAsync(); await Migrations.ApplyAsync(ConnectionString); } public Task ResetAsync() => Migrations.TruncateAsync(ConnectionString, "orders"); public Task DisposeAsync() => _container.DisposeAsync().AsTask();}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
A double of the database proves that your code calls the double correctly, not that the query works. Real behaviour lives in what doubles remove: SQL dialect, constraints, transaction isolation, generated keys and timeouts. Run the real engine in a disposable container built from the production migrations, and host the web app in-process so requests pass through routing, serialisation and middleware.
Start the container once per test collection, not per test, and reset only the rows each test needs. Tests must not depend on order or leave data behind. Seed only what a test reads, so a failure points at one fixture.
Do not stand in an in-memory provider for a relational database. Many such providers ignore constraints, transactions and query translation that production enforces, so the test passes where production fails. When NOT to: pure logic with no I/O belongs in unit tests; integration tests are slower and should cover the boundaries, not every branch.
Ask for it
- throwaway container
- test fixture
- in-process test host
- reset between tests
- seed data
- provider parity
Add an integration test project that starts a database container once per run, applies the production migrations, and hosts the Orders API in-process. Truncate the orders table before each test. Add a test that a duplicate order number returns 409 Conflict, proving the real unique index is enforced.