Lesson 24 / 25

Case Study: Testing an Order Service End to End

Combine unit, slice and integration tests into a balanced suite.

A balanced test suite

Consider an OrderService that prices a cart, applies coupons, charges a payment gateway, saves the order and sends a confirmation email. A balanced suite looks like this. Pure unit tests (plain JUnit, no mocks) cover the pricing and coupon rules with parameterised boundary cases: they are numerous, fast and stable. Service unit tests with Mockito cover orchestration: a declined payment saves nothing and sends no email; a successful payment saves a PAID order and sends exactly one email (verified with a captor); a broker failure after payment is handled correctly. Collaborators are interfaces (PaymentGateway, OrderRepository, Mailer) and time comes from an injected Clock. Slice tests check the REST controller's status codes, validation and JSON with @WebMvcTest and a @MockitoBean service. Repository tests check custom queries against real PostgreSQL with @DataJpaTest and Testcontainers. A few integration tests run the whole checkout with a real database and a WireMock payment provider. CI runs everything on each pull request, reports coverage, and runs PIT nightly on the pricing module.

The suite at a glance

Each layer tests what it is best at.

layer                     tool                              count   speed      what it proves
------------------------  --------------------------------  ------  ---------  ------------------------------------
pricing & coupon rules    JUnit + @ParameterizedTest        ~60     ms         business rules and boundaries
OrderService orchestration JUnit + Mockito (+ captors)       ~15     ms         calls, side effects, error handling
OrderController           @WebMvcTest + MockMvc             ~10     < 1 s      HTTP status, validation, JSON shape
OrderRepository queries   @DataJpaTest + Testcontainers     ~8      seconds    SQL, mappings, constraints
checkout flow             @SpringBootTest + Testcontainers  ~3      seconds    wiring, transactions, real DB
                          + WireMock payment provider
nightly                   PIT on pricing module             -       minutes    assertions catch behaviour changes

Push tests down the pyramid

When an integration test catches a bug in a pricing rule, also add a fast unit test for that rule. The next regression is then caught in milliseconds at the right level.

Quick check: In the case study, how is the rule "a declined payment must not send an email" best tested?

  • A service unit test with mocks that verifies the mailer is never called
  • A full end-to-end UI test
  • A JaCoCo report
  • A database migration
Answer

A service unit test with mocks that verifies the mailer is never called — Interaction-focused behaviour of the service is cheapest to check with Mockito and verify(never()).