Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · senior level

@MockitoBean, mocking discipline, and where mocks end and fakes/containers begin.

🎤 Say this first

@MockitoBean (Boot 3.4+, replacing the deprecated @MockBean) swaps a bean for a Mockito mock inside a Spring context — powerful, but every distinct mock combination forks a new context (cache miss = slow suite). Discipline: mock at architectural boundaries you own the contract for (ports: PaymentPort, mail, clock) — never mock what you don't own (mock the adapter's port, test the adapter itself against WireMock/Testcontainers); prefer fakes (in-memory implementations) for rich collaborators; and never mock value objects, entities, or the class under test's internals — over-mocked tests pass while prod burns.

The full picture

  • Mock vs fake vs container: mock = scripted interactions (verify a protocol); fake = working lightweight impl (in-memory repo — tests read naturally); container/WireMock = the real technology or a faithful HTTP double (test serialization, timeouts, error bodies). Choose by what the test must prove.
  • 'Don't mock what you don't own': mocking RestTemplate/S3Client binds tests to your guess of the library's behavior. Wrap it in your port (DocumentStore), mock the port in service tests, and integration-test the adapter against LocalStack/WireMock.
  • Context-cache economics: @MockitoBean changes the context key → suites with 30 different mock combos build 30 contexts. Consolidate into shared test configurations (@TestConfiguration with common fakes) to keep the cache hot.
  • Signals of over-mocking: verify-heavy tests asserting call sequences of internals; tests that break on refactors that didn't change behavior; mocks returning mocks.
Port-based design makes the choice for you
// The port (you own this contract):
public interface PaymentPort { Receipt charge(OrderId id, Money amount); }

// Service test: mock/fake the PORT — no HTTP anywhere
@Test void declinedPaymentReleasesInventory() {
    var payments = mock(PaymentPort.class);
    when(payments.charge(any(), any())).thenThrow(new PaymentDeclined("insufficient"));
    var service = new CheckoutService(new InMemoryInventory(), payments);
    assertThatThrownBy(() -> service.checkout(cart))
        .isInstanceOf(PaymentDeclined.class);
    assertThat(inventory.reserved()).isEmpty();          // the actual behavior
}

// Adapter test: the REAL client against WireMock — timeouts, 5xx, body shapes
@Test void gatewayTimeoutMapsToRetryableException() { … }

🔄 Likely follow-up questions

  • Why does mocking RestTemplate directly produce lying tests? Give a concrete miss.
  • How do fakes change test readability vs mocks — trade-offs?
  • How do you keep @MockitoBean from wrecking context caching?