Lesson 15 / 25

BDD Style and Mocking Guidelines

Write readable given-when-then tests with BDDMockito and avoid common mocking mistakes.

Readable tests and sensible mocking

BDDMockito offers aliases that read like behaviour specifications: given(catalog.priceOf("pen")).willReturn(price) for stubbing and then(mailer).should().send(any()) for verification, matching the given-when-then structure of a test. Beyond style, a few guidelines keep mock-based tests valuable. Do not mock what you do not own: mocking third-party APIs (HTTP clients, AWS SDKs) bakes your assumptions about them into tests; wrap them in your own small interface (an adapter) and test the adapter with integration tests. Do not mock value objects such as Money or LocalDate; create real ones. Do not mock the class under test. Avoid deep stubs (RETURNS_DEEP_STUBS chains) that mirror long call chains, a design smell. Prefer few mocks per test: needing many mocks often means the class has too many responsibilities. And remember that mocks verify that code calls collaborators as you expect, not that those collaborators work; integration tests are still needed.

Given-when-then with BDDMockito

The test reads like a specification of the behaviour.

import static org.mockito.BDDMockito.*;

@ExtendWith(MockitoExtension.class)
class LoyaltyServiceTest {

    @Mock PointsLedger ledger;
    @Mock Notifier notifier;

    @Test
    void goldMembersEarnDoublePointsAndAreNotified() {
        // given
        given(ledger.tierOf("c-42")).willReturn(Tier.GOLD);
        var service = new LoyaltyService(ledger, notifier);

        // when
        service.recordPurchase("c-42", Money.inr("1000.00"));

        // then
        then(ledger).should().addPoints("c-42", 200);          // 2 x 100 points
        then(notifier).should().pointsEarned("c-42", 200);
        then(notifier).shouldHaveNoMoreInteractions();
    }
}

// adapter around a third-party SDK: mock YOUR interface, integration-test the adapter
interface SmsSender { void send(String phone, String text); }
final class TwilioSmsSender implements SmsSender { /* calls the vendor SDK */ }

Rehearsing with stand-ins

Stand-ins are fine for rehearsing an actor's own scene, but you would never judge the whole play on a rehearsal where every other actor is a stand-in. Mock-heavy unit tests need real integration tests alongside them.

Quick check: According to common mocking guidelines, how should you test code that uses a third-party SDK?

  • Mock the SDK classes directly everywhere
  • Never test it
  • Wrap the SDK in your own interface, mock that interface in unit tests, and integration-test the adapter
  • Use static mocks for every SDK call
Answer

Wrap the SDK in your own interface, mock that interface in unit tests, and integration-test the adapter — Owning the interface keeps unit tests stable and isolates SDK assumptions in a tested adapter.