← Back to the section

External dependencies — HTTP services, mail providers, payment gateways — make tests slow and brittle. In this article we look at when you need a mock (a stub) and when it only gets in the way, and how to test HTTP calls with WireMock.

When a mock is appropriate

A mock is an object that pretends to be the real thing: it accepts calls and returns predefined responses. Mockito is the standard tool for creating mocks in Java tests.

Short rule of thumb: a mock is appropriate at an external boundary — where your code ends and someone else's begins.

Good candidates for a mock: an HTTP client to an external service, an SMS gateway, an email provider. In a unit test it is enough to confirm that your class reacts correctly to the dependency's response — spinning up a real server just for that is overkill.

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private PaymentClient paymentClient;

    @InjectMocks
    private OrderService orderService;

    @Test
    void failsWhenPaymentDeclined() {
        when(paymentClient.charge(any())).thenReturn(PaymentResult.DECLINED);

        assertThrows(PaymentDeclinedException.class,
            () -> orderService.place(orderRequest()));
    }
}

When a mock is harmful

Short rule of thumb: mocking your own code means testing your assumptions about behavior, not the behavior itself.

If a mock sits between two of your own classes (for example, OrderService and OrderRepository), the test passes even when they interact incorrectly. Change the signature or the meaning of a method — the mock "won't notice", and the test still turns green.

The rule: mocks only at external boundaries. For your own Repository, prefer Testcontainers with a real database: it costs more to start up, but the test is more honest.

WireMock: an HTTP stub instead of the real service

When your service calls an external HTTP service, spinning up the real server in tests is not an option. WireMock starts a local HTTP server and returns the responses you configure.

@SpringBootTest
@AutoConfigureWireMock(port = 0)
class PaymentGatewayClientTest {

    @Autowired
    private PaymentGatewayClient client;

    @Test
    void returnsDeclinedOnHttp402() {
        stubFor(post(urlEqualTo("/charge"))
            .willReturn(aResponse()
                .withStatus(402)
                .withHeader("Content-Type", "application/json")
                .withBody("{\"status\":\"DECLINED\"}")));

        PaymentResult result = client.charge(new ChargeRequest("card_123", 500));

        assertThat(result).isEqualTo(PaymentResult.DECLINED);
    }
}

@AutoConfigureWireMock(port = 0) — Spring picks a free port and substitutes it into the application properties automatically. The client talks to localhost rather than the real service.

WireMock lets you simulate latency (withFixedDelay), dropped connections, and sequences of responses — useful for checking retry and timeout logic.

Isolating test data

Tests must be independent: run order should not affect the result. There are three main approaches.

Transactional rollback — each test runs inside a transaction that is rolled back afterwards. Works well when you don't need to verify behavior on commit.

@SpringBootTest
@Transactional
class ProductRepositoryTest {
    // the database is clean for every test
}

Cleanup via @Sql — explicitly reset the state before a test or a set of tests. Slower, but it honestly covers several transactions.

@Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Test
void createsOrder() { ... }

Unique identifiers — generate a unique UUID right in the test so that different tests don't compete for the same records.

Determinism: time and randomness

A test with LocalDateTime.now() inside the code under test is non-deterministic: every run returns a different result, and you can't write a precise assertion. The solution is Clock from java.time.

// Configuration: a Clock bean in the main context
@Bean
public Clock clock() {
    return Clock.systemDefaultZone();
}

// The service receives Clock through the constructor
public class OrderService {

    private final Clock clock;

    private LocalDateTime now() {
        return LocalDateTime.now(clock);
    }
}

// The test substitutes a fixed Clock
@Test
void stampsOrderWithCreationTime() {
    Clock fixed = Clock.fixed(Instant.parse("2025-01-15T10:00:00Z"), ZoneOffset.UTC);
    var service = new OrderService(fixed, repository);

    Order order = service.place(orderRequest());

    assertThat(order.createdAt()).isEqualTo(LocalDateTime.of(2025, 1, 15, 10, 0));
}

The same goes for random number generators: pass a Random with a fixed seed through the constructor, don't create a new Random() inside the method.

In short

  • Mocks are appropriate only at external boundaries (HTTP client, email, payment gateway); mocking your own code makes the test brittle.
  • WireMock starts a local HTTP server and replaces the real external service without network calls.
  • Testcontainers with a real database is more reliable than a mock repository.
  • Isolate test data: transactional rollback, @Sql, or unique identifiers in every test.
  • LocalDateTime.now() inside your code is a source of non-determinism; replace it with a Clock injected through the constructor.

Further reading

  • The testing pyramid — which level of tests is responsible for what.
  • Integration testing — Testcontainers, @SpringBootTest, layered tests.
  • Testing in Spring — @WebMvcTest, @DataJpaTest, and other slices.