The Test Pyramid has guided software testing strategy for over a decade. But in modern software — especially microservices, APIs, and React frontends — some teams find a different shape, the Test Trophy, better reflects where testing value actually lives. This guide explains both models honestly, examines where each works and fails, and gives you a practical framework for choosing the right mix for your specific project with JUnit 6.
The Test Pyramid
Mike Cohn introduced the Test Pyramid in 2009. The model has three layers:

The Pyramid’s core message: Write lots of fast unit tests, fewer integration tests, and very few E2E tests. This minimises test suite cost (time + maintenance) while maximising coverage.
The Test Trophy
Kent C. Dodds proposed the Test Trophy in 2018. Its shape places integration tests at the centre, with static analysis at the base:

The Trophy’s core message: Integration tests give you the best return on investment because they test the way your software actually works — multiple units collaborating — without the fragility of E2E tests.
Applying Both Models with JUnit 6
Pyramid-Style: Heavy Unit Testing
// PYRAMID approach: the bulk of tests are pure unit tests.
// Logic is tested in complete isolation with Mockito.
@ExtendWith(MockitoExtension.class)
@Tag("unit")
class PricingEngineTest {
@InjectMocks private PricingEngine pricingEngine;
@Mock private TaxCalculator taxCalculator;
@Mock private DiscountService discountService;
@ParameterizedTest(name = "Price {0} with tax rate {1}% = {2}")
@CsvSource({
"100.00, 20, 120.00",
"50.00, 10, 55.00",
"0.00, 20, 0.00"
})
void priceWithTaxCalculatesCorrectly(double basePrice,
double taxRatePercent,
double expectedFinalPrice) {
// Stub tax calculation
when(taxCalculator.calculate(basePrice, taxRatePercent))
.thenReturn(basePrice * taxRatePercent / 100);
when(discountService.getDiscount(any())).thenReturn(0.0); // no discount
double finalPrice = pricingEngine.calculateFinalPrice(basePrice, taxRatePercent);
assertEquals(expectedFinalPrice, finalPrice, 0.001);
}
// Many more focused tests: edge cases, boundaries, error paths
@Test
void negativeBasePriceThrowsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class,
() -> pricingEngine.calculateFinalPrice(-1.0, 20.0));
}
}
Trophy-Style: Emphasis on Integration Tests
// TROPHY approach: integration tests form the bulk of the suite.
// Tests verify real-world behaviour through the HTTP API.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@Tag("integration")
class CheckoutApiTrophyStyleTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configurePostgres(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", postgres::getJdbcUrl);
r.add("spring.datasource.username", postgres::getUsername);
r.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private TestRestTemplate restTemplate;
// Trophy-style integration test covers happy path AND validation error
// in one focused scenario without mocking any internal components
@Test
@DisplayName("Valid checkout creates order, deducts stock, sends email")
void validCheckoutCreatesOrderDeductsStockAndSendsEmail() {
// Full real flow: controller → service → repository → real DB
// External email and payment are WireMocked at infrastructure level
CreateOrderRequest request = new CreateOrderRequest("[email protected]", "PROD-01", 1);
ResponseEntity<OrderDto> response =
restTemplate.postForEntity("/api/orders", request, OrderDto.class);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
assertEquals(OrderStatus.CONFIRMED, response.getBody().getStatus());
}
}
When the Pyramid Works Best
- ✅ Domain-heavy applications with complex business logic (insurance pricing, financial calculations, tax engines) where the logic itself is the most valuable thing to test
- ✅ Library or SDK development where the public API surface is the contract and pure functionality matters most
- ✅ Algorithmic code (sorting, parsing, graph algorithms) that benefits from hundreds of parameterized edge case tests
- ✅ Legacy codebases being refactored — unit tests lock in current behaviour before changes
When the Trophy Works Best
- ✅ CRUD-heavy REST APIs where most of the value is in verifying that HTTP → service → database roundtrips work correctly
- ✅ React/Node.js frontends (Kent Dodds’ original context) where rendering-level integration tests give more value than isolated component unit tests
- ✅ Microservices with thin service layers and most complexity at the integration boundary
- ✅ Projects where mocking is painful — if you spend more time maintaining mock setup than writing assertions, shift to integration tests
The Practical Hybrid: What Actually Works
Most production Java projects do best with a hybrid that borrows from both models:
| Test type | When to write it | JUnit 6 setup |
|---|---|---|
| Unit | Complex business logic, algorithms, validation, edge cases | @ExtendWith(MockitoExtension.class) |
| Slice (controller, JPA) | HTTP mapping, JSON serialisation, JPA queries | @WebMvcTest, @DataJpaTest |
| Integration | Full service flow with real DB (Testcontainers) | @SpringBootTest + Testcontainers |
| E2E | Critical user journeys only (checkout, registration) | DockerComposeContainer |
Frequently Asked Questions (FAQs)
Q1: Is the Test Pyramid outdated?
Not outdated, but not universal either. The Pyramid was designed for applications with thick domain layers where unit tests cover the most complex code. For API-centric applications, the Trophy or a hybrid better reflects where value lives. Both models are tools, not commandments. Use whichever shape produces the most useful feedback for your specific architecture.
Q2: How do I know if I have too many mocks and should shift to integration tests?
Watch for these signals: mock setup is longer than the actual test assertions, changing production code requires updating many mocks without any logic change, tests pass but bugs still reach production, or your tests are coupled to implementation details rather than behaviour. These are all signs that your unit tests are testing the wrong thing and integration tests would give more genuine confidence.
Q3: Can I mix Pyramid and Trophy-style tests in the same project?
Yes, and this is normal in real projects. Use Pyramid-style (heavy unit tests) for your domain and business logic packages. Use Trophy-style (heavy integration tests) for your API layer. The JUnit 6 tagging system lets you run each group independently so the different speeds do not conflict in your CI pipeline.
Q4: What does “static analysis” mean in the Test Trophy base layer?
In the Trophy model, static analysis (the base layer) includes tools like SpotBugs, PMD, Checkstyle, and SonarQube that catch bugs, code smells, and style violations without running the code. In Java, @SuppressWarnings and compiler warnings also belong here. These tools catch a class of errors (null dereferences, resource leaks, API misuse) that are difficult to catch with tests.
Q5: How many tests is too many?
There is no upper limit on tests — but there is a cost to each one. A test suite is too large when: it takes more than 5 minutes to run locally, making a simple change requires updating dozens of tests, or the suite has significant duplication across layers. If your test suite feels like a burden rather than an asset, audit which tests are providing unique value and which are testing the same behaviour in multiple layers unnecessarily.
See Also
- Unit vs Integration vs E2E Testing in JUnit 6 (Practical Guide)
- Tags and Test Suites in JUnit 6: Organizing Large Test Bases
- Writing Maintainable Tests in JUnit 6 (Clean Code Principles)
- JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing
- JUnit 6 Tutorial: Complete Series Index
Conclusion
Neither the Test Pyramid nor the Test Trophy is universally correct. The Pyramid works well for domain-heavy code with complex logic. The Trophy works well for API-centric applications where integration-level tests provide the most realistic coverage. In practice, most successful Java projects use a hybrid: many unit tests for complex domain logic, many integration tests for API flows, and a small set of E2E tests for critical user journeys. The specific shape matters less than the discipline of writing tests at the right layer for the right reason.
Next: Writing Maintainable Tests in JUnit 6 — clean code principles applied to test code so your test suite stays readable and valuable as the project grows.
No Comments yet!