Skip to content

Optimize integration test suite: replace @DirtiesContext with @Transactional and shared containers #54

Description

@Washingtonwei

Problem

The integration test suite currently takes 25–30 minutes to run. The main bottlenecks are:

  1. Excessive @DirtiesContext usage

    There are approximately 60 @DirtiesContext(methodMode = AFTER_METHOD) annotations across 14 test files.

    Each annotation destroys and recreates the full Spring application context, which takes roughly 5–10 seconds per recreation due to bean wiring, RSA key generation, and DataInitializer execution.

  2. Per-class Testcontainers setup

    Each integration test class declares its own @Container MySQL instance.

    Because each container produces different connection properties, Spring cannot effectively reuse cached application contexts. This results in approximately 17 separate Spring context creations.

  3. Repeated DataInitializer execution

    DataInitializer contains about 1,193 lines of seed data and runs every time a new Spring context is created.

    It currently runs approximately 78 times total:

    • ~17 initial context creations
    • ~60 additional recreations caused by @DirtiesContext

Proposed Solution

1. Use a singleton container pattern

Create a SharedContainers utility class that starts MySQL and Mailpit containers once per JVM.

All integration test classes should reference these shared containers instead of declaring their own per-class containers.

2. Introduce an abstract integration test base class

Create an AbstractIntegrationTest base class with @Transactional.

All integration tests should extend this class. Spring Test will roll back the transaction after each test method, restoring the database to the original DataInitializer-seeded state.

This allows us to remove @DirtiesContext from most or all integration tests.

This approach is valid because:

  • Tests use webEnvironment = MOCK with MockMvc
  • Test execution and controller/service execution occur in the same thread
  • The test transaction propagates correctly
  • Services do not use REQUIRES_NEW
  • DataInitializer runs during context startup and its committed data survives test rollback

3. Migrate all integration test classes

Update all 17 integration test classes to:

  • Extend AbstractIntegrationTest
  • Remove per-class container declarations
  • Remove @Testcontainers where no longer needed
  • Remove redundant Spring test annotations
  • Remove all unnecessary @DirtiesContext annotations

Expected Impact

Metric Before After
MySQL containers started ~17 1
Spring contexts created ~77 ~2
DataInitializer executions ~78 ~2
Estimated total runtime 25–30 min ~5 min

Accepted Tradeoff

Using @Transactional on integration tests keeps the Hibernate session open for the duration of each test.

As a result, these tests may no longer detect LazyInitializationException issues that would occur outside a transaction.

This is acceptable for this test suite because these integration tests primarily verify API contracts through HTTP request/response behavior. They are not intended to validate transaction boundary behavior.

If needed, a small number of targeted non-transactional tests can be added separately to verify lazy-loading or transaction-boundary behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions