| name | test-strategy |
| description | Test pyramid, testing strategy, and when to use which test type |
Martin Fowler - Testing Strategy
Apply Martin Fowler's testing philosophy for balanced, effective test suites.
Core Philosophy
The Test Pyramid
/\
/ \ E2E / UI Tests
/ \ (few, slow, brittle)
/โโโโโโ\
/ \ Integration Tests
/ \ (some, moderate speed)
/โโโโโโโโโโโโ\
/ \ Unit Tests
/ \ (many, fast, stable)
/โโโโโโโโโโโโโโโโโโ\
Key Insight: More tests at the bottom, fewer at the top.
Why a Pyramid?
| Level | Speed | Cost to Write | Cost to Maintain | Confidence |
|---|
| Unit | Fast (ms) | Low | Low | Component works |
| Integration | Medium (s) | Medium | Medium | Components work together |
| E2E | Slow (min) | High | High | System works for user |
The Anti-Pattern: Ice Cream Cone
โโโโโโโโโโโโโโ
/ \ Manual Testing
/โโโโโโโโโโโโโโโโ\
/ \ E2E Tests
/โโโโโโโโโโโโโโโโโโโโ\
/ \ Integration
โโโโโโโโโโโโโโโโโโโโโโโโ Unit (few)
Problem: Slow feedback, high maintenance, flaky tests.
Test Types Defined
Unit Tests
Test a single unit (class, function) in isolation.
@Test
void calculateDiscount_goldCustomer_returns15Percent() {
DiscountCalculator calc = new DiscountCalculator();
double discount = calc.calculate(100.0, CustomerType.GOLD);
assertEquals(15.0, discount, 0.01);
}
Characteristics:
- Fast (milliseconds)
- No I/O (database, network, filesystem)
- Test one thing
- Use test doubles for dependencies
Integration Tests
Test how components work together.
@Test
void userRepository_savesAndRetrieves() {
UserRepository repo = new UserRepository(testDatabase);
User user = new User("alice@test.com");
repo.save(user);
User found = repo.findByEmail("alice@test.com");
assertEquals("alice@test.com", found.getEmail());
}
Characteristics:
- Slower (seconds)
- Tests real interactions
- May use test containers or in-memory databases
- Tests component boundaries
E2E Tests
Test complete user journeys through the system.
@Test
void userCanCompleteCheckout() {
browser.navigateTo("/products");
browser.click("#add-to-cart-widget-1");
browser.click("#checkout");
browser.fillForm("#payment-form", validCard);
browser.click("#submit-order");
assertThat(browser.getCurrentUrl()).contains("/order-confirmation");
assertThat(browser.getText("#confirmation")).contains("Order placed");
}
Characteristics:
- Slow (minutes)
- Tests real user scenarios
- Most likely to be flaky
- Catches integration issues unit tests miss
When to Use Which
Use Unit Tests For
- Business logic - calculations, validations, transformations
- Edge cases - null handling, boundary conditions
- Algorithms - sorting, searching, parsing
- Fast feedback - run on every save
class PriceCalculator { }
class EmailValidator { }
class DateFormatter { }
Use Integration Tests For
- Database access - repositories, queries
- External services - APIs, message queues
- Component interactions - service layer calling repository
- Configuration - Spring wiring, dependency injection
class UserRepository { }
class PaymentGateway { }
class OrderService { }
Use E2E Tests For
- Critical user journeys - checkout, login, signup
- Smoke tests - "does the app start?"
- Contract verification - does the UI match the API?
- Regression for complex flows - multi-step wizards
- Complete checkout flow
- User registration and login
- Search and filter results
- Critical business workflows
Practical Guidelines
The 70/20/10 Rule (approximate)
- 70% Unit Tests - Fast, focused, many
- 20% Integration Tests - Key boundaries
- 10% E2E Tests - Critical paths only
Test at the Right Level
WRONG: E2E test for validation logic
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
browser.fillInput("#email", "invalid");
browser.click("#submit");
expect(browser.getText(".error")).toBe("Invalid email");
// Slow, brittle, overkill for simple validation
RIGHT: Unit test for validation logic
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@Test
void validate_invalidEmail_returnsFalse() {
assertFalse(EmailValidator.isValid("invalid"));
}
// Fast, focused, easy to maintain
Don't Duplicate Coverage
If unit tests cover the logic โโโบ Don't repeat in integration
If integration tests cover the flow โโโบ Don't repeat in E2E
The Sociable vs Solitary Unit Test
Solitary (London School): Mock all collaborators
@Test
void orderService_appliesDiscount() {
DiscountService mockDiscount = mock(DiscountService.class);
when(mockDiscount.calculate(any())).thenReturn(10.0);
OrderService orders = new OrderService(mockDiscount);
Order order = orders.create(items);
assertEquals(90.0, order.getTotal());
}
Sociable (Detroit School): Use real collaborators when simple
@Test
void orderService_appliesDiscount() {
DiscountService realDiscount = new DiscountService();
OrderService orders = new OrderService(realDiscount);
Order order = orders.create(items);
assertEquals(90.0, order.getTotal());
}
Fowler's View: Both are valid. Use solitary when collaborators are complex or slow. Use sociable when collaborators are simple and fast.
Test Isolation Strategies
Database Tests
@BeforeEach
void setUp() {
database = new H2Database();
}
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
@Transactional
@Test
void test() {
}
External Service Tests
PaymentClient mockClient = mock(PaymentClient.class);
@BeforeEach
void setUp() {
stubFor(post("/charge")
.willReturn(ok().withBody("{\"status\":\"success\"}")));
}
@Pact(consumer = "OrderService")
RequestResponsePact chargeCard(PactDslWithProvider builder) {
return builder
.given("valid card")
.uponReceiving("charge request")
.path("/charge")
.method("POST")
.willRespondWith()
.status(200)
.build();
}
Continuous Integration Strategy
Fast Feedback Loop
On every commit:
โโโ Unit tests (< 5 min)
โโโ Fast integration tests (< 10 min)
On PR/merge:
โโโ Full integration tests (< 30 min)
โโโ E2E smoke tests (< 15 min)
Nightly:
โโโ Full E2E suite (hours OK)
Test Selection
mvn test -Dgroups=unit
mvn test -Dgroups=unit,integration
mvn test
Code Review Checklist
Test Level
Test Quality
Coverage
Quick Reference
| Question | Answer |
|---|
| How to test business logic? | Unit test |
| How to test database queries? | Integration test |
| How to test API contracts? | Integration test with mocks |
| How to test user flows? | E2E test (sparingly) |
| How to test validation? | Unit test |
| How to test error handling? | Unit + integration |
| Tests too slow? | Move down the pyramid |
| Tests too brittle? | Move down the pyramid |
| Missing bugs in production? | Add integration tests |
Key Principles
- Write tests at the lowest level that gives confidence
- Unit tests for logic, integration tests for boundaries
- E2E tests are expensive - use sparingly for critical paths
- Fast tests run more often, catch bugs earlier
- Flaky tests destroy trust - fix or delete them
Resources