Integration Testing with Testcontainers: Mocking, Networking, and Aliases
Unit tests are fast, but integration tests usually provide the closest confidence to production. Testcontainers spins up real processes (databases, brokers, custom images) during tests and tears them down afterward—ideal for CI pipelines that have Docker available.
This post covers Testcontainers strategy, mocking boundaries, networking, and network aliases in detail.
What Is Testcontainers?
Testcontainers is a family of libraries (Java, Go, .NET, Node.js, Python, etc.) that programmatically starts ephemeral Docker containers for tests. When tests finish, resources are cleaned up. You need Docker (or a compatible runtime) locally and in CI.
Why Mocks Alone Are Not Enough
Mocks simulate external systems based on your assumptions. They miss real behavior: PostgreSQL version quirks, TLS, collations, timeouts, driver differences, and protocol edge cases.
Testcontainers + integration tests:
- Validate against real protocols and versions
- Catch migration and SQL compatibility issues
- Reduce “works in prod, mocked in tests” gaps
Where to Draw the Line: Mock vs Real
Good candidates for mocks
- Third-party billing APIs (cost, rate limits)
- OAuth providers (deterministic token scenarios)
- Time (
Clockinjection) - Randomness (seeded generators)
Good candidates for Testcontainers
- PostgreSQL, MySQL, MongoDB
- Redis, Kafka, RabbitMQ
- Elasticsearch, MinIO (S3-compatible)
- Your own service Docker images
Rule of thumb: if protocol/version fidelity matters, prefer real dependencies in tests; use fine-grained mocks or contract tests for business rules and unstable external APIs.
Diagram: Test Pyramid Placement
/\
/ \ E2E (few, slow)
/____\
/ \ Integration (Testcontainers shines here)
/________\
/ \ Unit (many, fast)
/____________\
Networking: How Containers See Each Other
Containers attach to a Docker bridge network by default. Containers on the same user-defined network can resolve each other by container name or network alias.
Diagram: Single Network, Two Services
+---------------- Docker Network (test-net) ----------------+
| |
| +-------------+ DNS: postgres, db-alias |
| | PostgreSQL |<------------------------------------+ |
| | :5432 | | |
| +-------------+ | |
| ^ | |
| | jdbc/postgres://postgres:5432 | |
| | | |
| +-----+-------------+ | |
| | App under test | | |
| | (JUnit / Go test) | | |
| +-------------------+ | |
| |
+-----------------------------------------------------------+
Your app should use the service hostname (name or alias), not hard-coded localhost, when talking container-to-container.
What Are Network Aliases For?
Aliases add extra DNS names for a container on a network. Useful when:
- Config expects a fixed hostname (
db.internal,kafka:9092) - You run multiple instances of the same image and need distinct names
- You want parity with legacy docker-compose service names
Example intent: if your app expects jdbc:postgresql://db-alias:5432/app, attach alias db-alias to the Postgres container so you do not special-case URLs only for tests.
Host Access: Mapped Ports
The test process usually runs on the host. Container port 5432 is published as localhost:RANDOM. Testcontainers exposes this via getMappedPort(5432) (API name varies by language).
Two models:
- Test on host → container: use
127.0.0.1:mappedPort. - Container → container: same network, use internal port + DNS name; do not mix in mapped ports.
Parallelism and Isolation
When running parallel jobs or parallel test suites:
- Prefer separate networks or unique container names per suite
- Use unique DB names or schemas (
test_${uuid}) - Avoid
reusein CI unless you fully understand cleanup implications
Otherwise you risk port clashes and cross-test data leakage.
Hybrid Pattern: Testcontainers + Mocks
[Test]
|
+---> PostgreSQL (Testcontainers) -----> real SQL
|
+---> PaymentClient (mock/stub) -----> no external API
Keep data real; mock expensive or flaky outbound calls—common pattern for high-signal tests.
Example Flow: API + DB Integration Test
- Start Postgres container in
@BeforeAll/TestMain - Apply schema (Flyway/Liquibase or raw SQL)
- Point connection pool at
mappedPortor network hostname - Call API via HTTP client; assert DB state
- Stop container (often automatic via library lifecycle)
Critical Considerations
1) Docker in CI
GitHub Actions, GitLab CI, etc. need Docker socket access or DinD. Document the requirement in README.
2) Cold Start Time
First image pulls are slow. Use layer caching and CI image caches when possible.
3) Readiness
Use waitFor strategies; open port ≠ ready service.
4) Security
Pin images by digest (postgres:16-alpine@sha256:...). Avoid latest in CI.
Common Mistakes
- Confusing host networking with container-to-container DNS
- Sharing ports/volumes across parallel tests
- Over-mocking and getting false confidence
- Forgetting aliases so app config hostnames never resolve in tests
Conclusion
Testcontainers makes integration tests realistic and repeatable. You do not need to ban mocks—use them at the right boundary. Solid understanding of networking and aliases saves hours when testing multi-container setups.
Short version: test the protocol for real, mock the expensive and uncontrollable world.