Test Strategy: TestContainers & TDD

How Vivolar uses TestContainers for integration tests, the test pyramid, and why we never mock the database.

testing testcontainers tdd spring-boot

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:

This means our integration tests validate:

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:

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:

  1. Red — write a failing test that describes the expected behavior
  2. Green — write the minimum code to make it pass
  3. Refactor — clean up while keeping tests green
  4. Verify — run ./mvnw verify to catch any regressions

The build gate is strict: never commit red code. If tests fail, you fix them before committing.