Skip to main content

Designing for Testability in Spring (Controller → Handler → Service)

When building applications with Spring Boot/WebFlux, testability often comes down to code structure and design choices. Below are recommendations that balance clean design, maintainability, and ease of testing.


1. Keep Layers Simple and Focused

  • Controller → Handler → Service → Repository is a good separation.
  • Each layer has one responsibility:
    • Controller: request/response mapping, validation.
    • Handler: orchestrate workflows, call multiple services.
    • Service: business logic, domain rules, persistence.
  • Tests then focus on each layer separately:
    • Controllers with slice tests.
    • Services with unit tests.
    • End-to-end wiring with integration tests.

2. Favor Dependency Injection with Interfaces

  • Define contracts (UserService, OrderRepository) as interfaces.
  • Inject implementations into higher layers.
  • Tests can swap real beans for mocks (@MockBean) or fakes easily.

3. Design for Composition, Not Inheritance

  • Prefer smaller, composable handlers/services over large, monolithic ones.
  • Compose workflows instead of deep inheritance trees.
  • Makes it easier to test small parts in isolation.

4. Avoid Static State and Hard Coupling

  • Static utils that talk to DB, files, or environment are hard to test.
  • Wrap them in injectable services (TimeProvider, IdGenerator).
  • Tests can provide predictable mocks.

5. Use Test Doubles Wisely

  • Mocks: verify interactions (verify(service).save(..)).
  • Stubs/Fakes: return canned values, simpler than mocks.
  • Prefer fakes when possible (less brittle).

6. Keep Business Logic out of Controllers

  • Controllers should delegate.
  • Real rules live in services → tested without HTTP layer.
  • Controller tests then only check mapping/validation.

7. Adopt Pragmatic Patterns

  • Strategy/Registry: for pluggable behaviors (document handlers, etc.).
  • Command/Query handlers: if domain complexity justifies.
  • Keep patterns shallow—avoid factories of factories.

8. Testing Pyramid

  • Unit tests: fast, isolated, no Spring context.
  • Slice tests (@WebFluxTest, @DataMongoTest): cover one layer + mocks.
  • Integration tests (@SpringBootTest + Testcontainers): realistic, minimal mocks.
  • E2E tests: real HTTP + DB, no mocks.

9. Refactor for Testability

If mocking feels painful:

  • The class might have too many responsibilities.
  • Dependencies may be hidden.
  • Split big classes and expose contracts.

10. Balance Complexity vs Pragmatism

  • Avoid over-engineering (heavy DDD, CQRS) unless justified.
  • Don’t abstract everything “just in case”.
  • Optimize for clarity and predictability.

✅ Takeaway

The best structure for testability:

  • Thin, focused layers.
  • Clear contracts (interfaces).
  • Push logic into services.
  • Controllers delegate.
  • Use stubs/fakes for speed, mocks for precision.
  • Apply patterns only when they reduce duplication.