Integration Testing
What it is
Section titled “What it is”An integration test verifies that two or more units work together — your code plus a real, or realistically faked, collaborator. Where a unit test replaces everything around the subject, an integration test lets the seams be real: a database, a message queue, an HTTP layer, another module.
Integration tests catch the bugs unit tests structurally cannot: mismatched contracts, wrong SQL, misconfigured serialization, wrong transaction boundaries.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- The boundary between your code and a datastore, queue, cache or external HTTP API
- Wiring: dependency injection, configuration, migrations
- Multi-step flows inside one deployable (e.g. controller → service → repository)
Avoid it when
- The concern is pure logic → a fast unit test is better
- The whole system across the network must be exercised → that is end-to-end
- The dependency is another team’s service you cannot run → use contract testing
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| Testcontainers | Multi (Java, Go, Node, Python, .NET) | Real DB/queue/broker in Docker, disposed after the test |
| Supertest | Node.js | Drive an HTTP server in-process |
| Spring Boot Test | Java | @SpringBootTest, slices, embedded DB support |
| WireMock | Multi | Stub HTTP dependencies |
| MockServer | Multi | Expect/verify HTTP interactions |
| pytest + testcontainers | Python | Same containers, Python fixtures |
httptest |
Go | In-process HTTP servers for tests |
| Docker Compose | Any | Bring up a dependency graph for the suite |
Quick setup
Section titled “Quick setup”npm install -D vitest testcontainers supertestimport { PostgreSqlContainer } from '@testcontainers/postgresql';
const pg = await new PostgreSqlContainer().start();const url = pg.getConnectionUri(); // real PostgreSQL, per runpip install pytest testcontainersgo get github.com/testcontainers/testcontainers-go<dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.20.1</version> <scope>test</scope></dependency>dotnet add package Testcontainers.PostgreSqldotnet add package Microsoft.AspNetCore.Mvc.TestingExample
Section titled “Example”import request from 'supertest';import { app } from '../src/app';
it('creates and reads back a user', async () => { const created = await request(app) .post('/users') .send({ name: 'Ada' }) .expect(201);
const res = await request(app) .get(`/users/${created.body.id}`) .expect(200);
expect(res.body.name).toBe('Ada');});import pytestfrom testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")def db(): with PostgresContainer("postgres:16") as pg: yield pg.get_connection_url()
def test_insert_and_read(db): repo = UserRepo(db) uid = repo.create(name="Ada") assert repo.get(uid)["name"] == "Ada"func TestCreateAndReadUser(t *testing.T) { srv := httptest.NewServer(app.Router()) defer srv.Close()
res, err := http.Post(srv.URL+"/users", "application/json", strings.NewReader(`{"name":"Ada"}`)) if err != nil || res.StatusCode != http.StatusCreated { t.Fatalf("create failed: %v %d", err, res.StatusCode) }}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class UserApiTest { @Autowired TestRestTemplate rest;
@Test void createsAndReadsBackUser() { var created = rest.postForEntity("/users", new User("Ada"), User.class); assertEquals(HttpStatus.CREATED, created.getStatusCode()); }}public class UserApiTests : IClassFixture<WebApplicationFactory<Program>>{ private readonly HttpClient _client; public UserApiTests(WebApplicationFactory<Program> f) => _client = f.CreateClient();
[Fact] public async Task CreatesAndReadsBackUser() { var created = await _client.PostAsJsonAsync("/users", new { Name = "Ada" }); Assert.Equal(HttpStatusCode.Created, created.StatusCode); }}Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Fake the boundaries you cannot own. Use Testcontainers or a real dev DB; mock only genuinely external, uncontrollable services.
- Isolate per test. Roll back transactions or recreate schema per test so runs are independent and parallelizable.
- Seed with builders. Factory functions beat a shared
fixtures.sqlthat drifts. - Do not assert on internals of a dependency. Assert on your behaviour as observed through the boundary.
- Keep the suite fast enough to run often. Integration tests are slower than units; prune duplicates and run them in CI on every push.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.