Best Practices for Mockito in Real-World Projects

Illustration for Best Practices for Mockito in Real-World Projects
By Last updated:

Mockito is one of the most popular Java mocking frameworks, widely adopted in enterprise and open-source projects. While getting started with Mockito is easy, writing maintainable, scalable, and production-ready tests requires discipline and following best practices.

This guide explores best practices for using Mockito in real-world Java projects, from writing clean unit tests to integrating with CI/CD pipelines.


Why Mockito Best Practices Matter

  • Prevent brittle tests that break with minor code changes.
  • Ensure maintainability in large test suites.
  • Improve readability for teams working on shared codebases.
  • Support CI/CD pipelines with reliable, deterministic tests.

Think of Mockito as a Swiss Army knife — powerful, but only if you use the right tool for the right job.


Best Practice 1: Prefer Constructor Injection

Mockito works best when classes use constructor injection for dependencies.

class OrderService {
    private final PaymentGateway paymentGateway;

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

    boolean placeOrder(String item) {
        return paymentGateway.process(item);
    }
}

Constructor injection makes it easy to inject mocks using @InjectMocks and avoids issues with hidden dependencies.


Best Practice 2: Use @ExtendWith(MockitoExtension.class)

Always initialize mocks with the JUnit 5 extension:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    PaymentGateway paymentGateway;

    @InjectMocks
    OrderService orderService;

    @Test
    void shouldProcessOrder() {
        when(paymentGateway.process("Book")).thenReturn(true);

        assertTrue(orderService.placeOrder("Book"));
    }
}

This ensures fresh mocks for each test run.


Best Practice 3: Verify Behavior, Not Implementation

Bad test (too coupled to internals):

verify(paymentGateway, times(1)).process("Book");

Better test (focus on outcome):

assertTrue(orderService.placeOrder("Book"));

Verification is powerful but overusing it creates fragile tests.


Best Practice 4: Keep Tests Focused

  • Test one behavior per test.
  • Avoid mixing unrelated verifications.
  • Use descriptive test names like shouldSendEmailOnRegistration.

Focused tests are easier to read and maintain.


Best Practice 5: Avoid Over-Stubbing

Don’t stub methods that are never used.

❌ Bad:

when(paymentGateway.process("Pen")).thenReturn(false);

✅ Good:

when(paymentGateway.process("Book")).thenReturn(true);

Unnecessary stubbing adds noise.


Best Practice 6: Use Argument Matchers Wisely

Mockito provides any(), eq(), and argThat(). Avoid mixing matchers with raw values.

verify(paymentGateway).process(eq("Book"));

This improves readability and avoids errors.


Best Practice 7: Reset Mocks Sparingly

Prefer fresh mocks per test instead of resetting:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock PaymentGateway paymentGateway;
    @InjectMocks OrderService orderService;
}

reset() is usually a code smell — use clearInvocations() if needed.


Best Practice 8: Use Spies for Legacy Code

Spies allow partial mocking when working with legacy systems.

List<String> list = spy(new ArrayList<>());
list.add("Hello");
verify(list).add("Hello");

But prefer refactoring over heavy spying.


Best Practice 9: Combine Mockito with Testcontainers

Use Mockito for unit tests and Testcontainers for integration tests:

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

Together, they cover both mocked dependencies and real-world systems.


Best Practice 10: Integrate with CI/CD

  • Run unit tests with mocks on every commit.
  • Run Testcontainers-based integration tests nightly or in staging.
  • Collect code coverage reports with JaCoCo.

This ensures speed + reliability.


Version Tracker

  • JUnit 4 → JUnit 5: Migration to @ExtendWith(MockitoExtension.class).
  • Mockito Updates: Static and final method mocking support.
  • Testcontainers Growth: Wider support for cloud-native dependencies.

Conclusion & Key Takeaways

Mockito is a powerful framework, but misuse leads to fragile tests. Following best practices ensures tests are robust, readable, and production-ready.

Key Takeaways:

  • Prefer constructor injection.
  • Keep tests focused and outcomes-driven.
  • Use mocks, spies, and matchers responsibly.
  • Avoid over-stubbing and unnecessary resets.
  • Combine with Testcontainers for full coverage.

FAQ

1. Should I verify every method call?
No, focus on outcomes, not implementation details.

2. Is using reset() in Mockito good practice?
Usually no — prefer fresh mocks per test.

3. Can I mock static methods in Mockito?
Yes, since Mockito 3.4+ using mockStatic().

4. How does @InjectMocks work?
It injects mocks into the class under test via constructors, setters, or fields.

5. Should I use spies often?
No, they’re best for legacy code where refactoring is hard.

6. Can Mockito and Testcontainers be used together?
Yes — Mockito for isolation, Testcontainers for real-world validation.

7. How do I avoid flaky tests?
Avoid over-stubbing and reset; keep tests independent.

8. Is Mockito enough for microservices?
Use Mockito for unit tests; pair with contract tests and Testcontainers.

9. How do I measure test coverage?
Use JaCoCo with JUnit 5 and Mockito.

10. Can I use Mockito in CI/CD pipelines?
Absolutely — it integrates seamlessly with Maven, Gradle, and Jenkins/GitHub Actions.