Test Strategy: TestContainers & TDD
How Vivolar uses TestContainers for integration tests, the test pyramid, and why we never mock the database.
Philosophy
Vivolar’s test strategy is built on one principle: test against real infrastructure whenever possible. Mocking databases, message brokers, or external APIs creates a false sense of confidence. The tests pass, but production breaks.
This approach was formalized in ADR-002, which established TestContainers as the standard for integration testing.
TestContainers Integration
Every integration test runs against a real PostgreSQL instance managed by TestContainers. The setup is straightforward:
- A shared PostgreSQL container starts once per test suite
- Flyway runs all migrations against it, just like production
- Each test class gets a transactional context that rolls back after each test
- The container is automatically cleaned up when the JVM exits
This means our integration tests validate:
- Flyway migrations actually work
- JPA mappings match the real schema
- SQL queries return correct results against real PostgreSQL (not H2 with its quirks)
- Constraint violations are caught before deployment
Test Pyramid
The project follows a practical test pyramid:
Unit tests — pure business logic, no Spring context. Fast, focused, and numerous. Used for validators, mappers, and domain calculations.
Integration tests — Spring Boot test slices with TestContainers. Test the full request → controller → service → repository → database flow. These are the backbone of our confidence.
No end-to-end tests yet — the frontend is tested with Vitest for component logic, but full E2E browser tests are a future addition. The API integration tests cover the critical paths.
Why Not Mock the Database?
We got burned early: mocked repository tests passed but a real migration had a column type mismatch. The test was green, production was red.
After that, the rule became simple:
- Repository tests → always TestContainers
- Service tests with DB queries → always TestContainers
- Pure logic tests → mocks are fine (no DB involved)
The cost is slightly slower tests (~30 seconds for container startup on first run, reused thereafter). The benefit is tests that actually prove the code works.
Test Organization
Tests follow the same package structure as production code:
src/test/java/com/vivolar/
├── inventory/
│ ├── controller/ItemControllerTest.java
│ ├── service/ItemServiceTest.java
│ └── repository/ItemRepositoryTest.java
├── household/
│ ├── service/HouseholdServiceTest.java
│ └── ...
└── common/
└── AbstractIntegrationTest.java ← shared TestContainers config
AbstractIntegrationTest provides the shared container, test profile configuration, and common utilities. All integration tests extend it.
The TDD Loop
Every feature follows the TDD cycle:
- Red — write a failing test that describes the expected behavior
- Green — write the minimum code to make it pass
- Refactor — clean up while keeping tests green
- Verify — run
./mvnw verifyto catch any regressions
The build gate is strict: never commit red code. If tests fail, you fix them before committing.