Mocks is holding your tests back -Testcontainers set them free (P2)

3 min read
zen8labs Mocks is holding your tests back -Testcontainers set them free (P2)

Writing a Real DAL Integration Test with Testcontainers 

Let’s say you have a simple UserRepository with CRUD logic. 

We’ll write a test that: 

  1. Starts PostgreSQL in a Docker container 
  2. Applies database schema 
  3. Runs real repository operations 
  4. Verifies real database behavior 

1. Create the PostgreSQLContainer 

@Testcontainers 
public class UserRepositoryTest { 
 
    @Container 
    static PostgreSQLContainer<?> postgres = 
            new PostgreSQLContainer<>("postgres:16-alpine") 
                .withDatabaseName("testdb") 
                .withUsername("test") 
                .withPassword("test"); 

Why static? 

Because it starts once for the entire test class, making tests much faster. 

2. Register Database Properties in Spring Boot 

If using Spring Boot JPA: 

@DataJpaTest 
@Testcontainers 
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 
public class UserRepositoryTest { 
 
    @Container 
    static PostgreSQLContainer<?> postgres = 
            new PostgreSQLContainer<>("postgres:16-alpine"); 
 
    @DynamicPropertySource 
    static void overrideProps(DynamicPropertyRegistry registry) { 
        registry.add("spring.datasource.url", postgres::getJdbcUrl); 
        registry.add("spring.datasource.username", postgres::getUsername); 
        registry.add("spring.datasource.password", postgres::getPassword); 
    } 
} 

Now your repository connects to the Testcontainers PostgreSQL automatically. 

3. Writing the Actual Test 

Example CRUD test: 

@Autowired 
UserRepository userRepository; 
 
@Test 
void testCreateUser() { 
    User user = new User(null, "alice", "alice@example.com"); 
 
    User saved = userRepository.save(user); 
 
    assertNotNull(saved.getId()); 
    assertEquals("alice", saved.getUsername()); 
} 

Foreign key test: 

@Test 
void testForeignKeyViolation() { 
    Order order = new Order(null, 9999L, "ITEM-001"); // 9999 = non-existent user ID 
 
    assertThrows(DataIntegrityViolationException.class, () -> { 
        orderRepository.save(order); 
    }); 
} 

This test would NEVER fail in a mocked setup. 

Best Practices for Using Testcontainers 

1. Use Alpine-based images 

They start significantly faster. 

postgres:16-alpine 

2. Use static containers per test class 

Reduces startup cost from ~2 seconds → ~200ms per test. 

3. Use reusable containers locally (optional) 

Create file: 

~/.testcontainers.properties 

Add: 

testcontainers.reuse.enable=true 

⚠ Not recommended for CI. 

4. Keep migrations small and precise 

Large SQL migrations slow down every integration test. 

Break the schema into modules, and use schema migration tools like Liquibase or Flyway to make sure the test database has the same structure as the real one. 

5. Separate unit tests from integration tests 

Use different folders: 

src/test/java           → unit tests 
src/integrationTest/java → Testcontainers tests 

Or use tags: 

@Tag("integration") 

Running Testcontainers on CI/CD 

zen8labs Mocks is holding your tests back -Testcontainers set them free (P2)

Yes — As long as the CI agent environment can run Docker, it would be easy peasy. 

Here is a simple workflow using GitHub Action. 

But before rolling into the action, remember that you should not run integration tests all the time, since the test itself is expensive in resource. Here is what you can consider doing: 

Event What runs Purpose 
Pull Request (merge request) Unit tests only Fast, cheap, catches logic errors before merging 
After merge → on main branch Integration tests (Testcontainers) + Build artifact Only run expensive tests when code is about to be released 

1. GitHub Actions Workflow File 

In .github/workflows/pr-tests.yml 

name: Pull Request - Unit Tests 
 
on: 
  pull_request: 
    branches: [ main ] 
 
jobs: 
  unit-tests: 
    runs-on: ubuntu-latest 
 
    steps: 
      - name: Checkout code 
        uses: actions/checkout@v4 
 
      - name: Set up Java 
        uses: actions/setup-java@v4 
        with: 
          distribution: temurin 
          java-version: 21 
 
      - name: Run Unit Tests Only 
        run: mvn -B -DskipITs=true test 

HOW it works 

  • You mark integration tests with Maven failsafe (e.g., *IT.java). 
  • Unit tests run only with mvn test (failsafe won’t run unless you run the integration test phase). 
  • The PR stays fast and does not pull Docker/Testcontainers. 

2. Main branch workflow – integration tests + build artifact 

File: .github/workflows/main-release.yml 

name: Main Branch - Integration Tests and Build 
 
on: 
  push: 
    branches: [ main ] 
 
jobs: 
  integration-tests-and-build: 
    runs-on: ubuntu-latest 
 
    steps: 
      - name: Checkout code 
        uses: actions/checkout@v4 
 
      - name: Set up Java 
        uses: actions/setup-java@v4 
        with: 
          distribution: temurin 
          java-version: 21 
 
      - name: Run Integration Tests (Testcontainers) 
        run: mvn -B verify 
 
      - name: Package Application JAR 
        run: mvn -B -DskipTests package 
 
      - name: Upload Artifact 
        uses: actions/upload-artifact@v4 
        with: 
          name: app-jar 
          path: target/*.jar 

HOW it works 

  • Running mvn verify triggers: 
    • Unit tests (mvn test) 
    • Integration tests (failsafe:integration-test + failsafe:verify) 
  • Testcontainers automatically spins up Docker DB/container environments. 
  • After tests pass, Maven builds your production JAR. 
  • GitHub uploads the artifact. 

Conclusion 

Mocking the database layer is a thing of the past. For real reliability, you need real database behavior, and Testcontainers makes that incredibly easy — whether locally or in CI pipelines. 

With: 

  • JUnit integration 
  • Real database isolation 
  • Wide range of framework support, Spring Boot included 
  • Can easily be integrated to any CI tools with Docker 

…Testcontainers gives Java developers a powerful, modern testing workflow that dramatically improves code quality. 

If you found this post useful, consider checking out the Testcontainers Developer Guide or exploring sample apps on GitHub to deepen your understanding. 

For backend applications that rely heavily on database operations, Testcontainers is one of the highest-ROI tools you can add to your engineering stack. If your team is looking to improve development workflows, automate testing pipelines, or adopt modern tooling like Testcontainers at scale, zen8labs provides engineering-focused consulting and technical solutions to help you get there faster. 

Luan Dinh, Software Engineer 

Related posts

Testing DAOs with mocks creates false. Learn how Testcontainers enables real PostgreSQL integration testing to catch issues earlier.
4 min read
In the rapidly evolving corporate world, organizations are increasingly recognizing the importance that the role of AI brings alongside the principles of ESG.
4 min read
When you need to split your data into different areas then you need to Partition. Learn here why partitioning matters for your projects and how you can do it
12 min read