Introduction to Mocking: Why Do We Need Mocks?

Illustration for Introduction to Mocking: Why Do We Need Mocks?
By Last updated:

When writing unit tests, the goal is to test a class or method in isolation. But in real-world applications, classes often depend on external systems like databases, APIs, message queues, or other services. Testing against these dependencies directly is slow, unreliable, and brittle. This is where mocking comes in.

Mocks are stand-ins for real dependencies. They allow you to simulate behavior, control outputs, and verify interactions without relying on actual external systems. By using mocks, you can write fast, reliable, and maintainable tests.


Why Do We Need Mocks?

  1. Isolation: Test only the unit under test, not its dependencies.
  2. Performance: Avoid slow external calls (e.g., database queries).
  3. Reliability: Prevent flaky tests caused by network or service outages.
  4. Flexibility: Simulate edge cases (timeouts, exceptions) that are hard to reproduce.
  5. CI/CD Ready: Ensure tests run consistently in pipelines.

Think of mocks as understudies in a play: they act out roles so the main actor (your class under test) can perform smoothly.


Mocking with Mockito and JUnit 5

Mockito is the most popular Java mocking framework. Let’s see it in action.

Example: Service with Dependency

class PaymentGateway {
    boolean process(double amount) {
        // Imagine this calls an external system
        return true;
    }
}

class OrderService {
    private final PaymentGateway gateway;

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    boolean placeOrder(double amount) {
        return gateway.process(amount);
    }
}

Testing with a Mock

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    @Test
    void shouldPlaceOrderSuccessfully() {
        PaymentGateway gateway = mock(PaymentGateway.class);
        when(gateway.process(100.0)).thenReturn(true);

        OrderService service = new OrderService(gateway);
        boolean result = service.placeOrder(100.0);

        assertTrue(result);
        verify(gateway).process(100.0);
    }
}

Here, the mock simulates the PaymentGateway without calling a real payment system.


Stubbing with Mockito

Stubbing defines what a mock should return.

when(mock.method()).thenReturn(value);
when(mock.method()).thenThrow(new RuntimeException("Error"));

This is powerful for simulating happy paths and failure scenarios.


Argument Matchers

Mockito allows flexible stubbing with argument matchers.

when(gateway.process(anyDouble())).thenReturn(true);

This covers all double inputs instead of hardcoding values.


Mocking Edge Cases

Mocks can simulate failures:

when(gateway.process(anyDouble())).thenThrow(new RuntimeException("Service down"));

This allows testing exception handling without breaking real systems.


Mocks vs Spies

  • Mocks: Replace the real object completely.
  • Spies: Wrap the real object but let some methods run while mocking others.
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);

spyList.add("Hello");
verify(spyList).add("Hello");

Real-World Example with Testcontainers

Sometimes you need integration tests alongside mocks.

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;

class ContainerIntegrationTest {

    @Test
    void shouldStartDatabase() {
        try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")) {
            postgres.start();
            assert postgres.isRunning();
        }
    }
}

You could mock queries for unit tests and use Testcontainers for integration tests.


Best Practices for Using Mocks

  • Mock only external dependencies, not the class under test.
  • Avoid over-mocking, which makes tests fragile.
  • Combine mocks with real integration tests for balance.
  • Use clear naming conventions to distinguish mocks from real objects.
  • Validate interactions with verify().

Version Tracker

  • JUnit 4 → JUnit 5: Improved annotations and integration with Mockito extensions.
  • Mockito Updates: Support for mocking static, final, and constructor methods.
  • Testcontainers Growth: Expanded support for real integration environments.

Conclusion & Key Takeaways

Mocks allow developers to write fast, isolated, and reliable tests by simulating external dependencies. With JUnit 5 and Mockito, mocking becomes straightforward and powerful. By combining mocks with Testcontainers, you can achieve both unit test speed and integration test reliability.

Key Takeaways:

  • Mocks simulate external dependencies.
  • Use Mockito for flexible mocking.
  • Combine mocks and real integration tests.
  • Mock edge cases to test resilience.

FAQ

1. What’s the difference between unit and integration tests?
Unit tests isolate code with mocks; integration tests use real systems.

2. How do I mock a static method in Mockito?
Use mockStatic() available in Mockito 3.4+.

3. How can Testcontainers help in CI/CD pipelines?
They provide disposable containers for reproducible integration testing.

4. What is the difference between TDD and BDD?
TDD is implementation-driven; BDD focuses on behavior specification.

5. How do I fix flaky tests in Java?
Remove external dependencies and use mocks for isolation.

6. Should I mock databases in unit tests?
Yes, databases belong in integration tests.

7. Can mocks return different values on consecutive calls?
Yes, with thenReturn(val1, val2, …).

8. Do mocks improve test speed?
Yes, by avoiding slow I/O and network calls.

9. Is it possible to verify method call counts?
Yes, verify(mock, times(2)).method();.

10. Should I migrate from JUnit 4 to JUnit 5 for mocks?
Yes, JUnit 5 integrates more cleanly with Mockito.