← Back to DevBytes

Mockito: Complete Testing Guide for Developers

Introduction to Mockito

Mockito is an open-source mocking framework for Java that enables developers to create and configure mock objects for unit testing. It provides a clean, fluent API that allows you to isolate the code under test by replacing real dependencies with simulated objects that return predefined responses or verify interactions. Originally inspired by the EasyMock framework, Mockito has evolved into the de facto standard mocking library in the Java ecosystem, with support for both Java and Kotlin applications.

At its core, Mockito allows you to:

Mockito integrates seamlessly with JUnit, TestNG, and other testing frameworks. It is typically added as a dependency via Maven or Gradle, and requires no complex configuration to get started. The framework emphasizes clean test code by avoiding the need for record/replay cycles and by providing a natural, readable syntax that mirrors the arrange-act-assert pattern.

Why Mockito Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In modern software development, unit tests are the foundation of code quality and maintainability. However, real-world applications rarely consist of isolated classes with no dependencies. Most production code interacts with databases, web services, file systems, message queues, and other external systems. Testing these integrations directly leads to slow, brittle, and non-deterministic tests. Mockito addresses these challenges by allowing you to replace real dependencies with controlled substitutes.

Isolation of Code Under Test

When you write a unit test, you want to verify the behavior of a single unit of code—typically a class or method—in complete isolation. Without mocking, a failure in a downstream dependency can cause your unit test to fail, even when the code under test is perfectly correct. Mockito ensures that your tests only break when the logic within the tested unit changes, not when an external service becomes unavailable or a database schema migrates.

Speed and Determinism

Tests that hit real databases or network endpoints are orders of magnitude slower than pure unit tests. A test suite with hundreds of such tests can take minutes or even hours to run, discouraging developers from running them frequently. Mockito-based tests execute in milliseconds because they operate entirely in memory. Moreover, mock-based tests are deterministic—they produce the same result every time, regardless of the state of external systems, network latency, or time of day.

Testing Edge Cases and Error Paths

Simulating rare error conditions like network timeouts, database connection failures, or malformed API responses is extremely difficult with real dependencies. Mockito lets you easily configure mocks to throw exceptions, return empty results, or produce unexpected data, enabling comprehensive coverage of error-handling code that would otherwise remain untested.

Test-Driven Development

Mockito facilitates a strict TDD workflow. When writing a new feature, you can mock out dependencies that don't yet exist, allowing you to design and test the interface contracts before implementing the concrete classes. This promotes clean architecture and interface-based design.

Getting Started with Mockito

Adding Mockito to Your Project

For Maven projects, add the following dependency to your pom.xml file. Mockito-core contains the core mocking library, while mockito-junit-jupiter provides integration with JUnit 5:

<!-- pom.xml -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.12.0</version>
    <scope>test</scope>
</dependency>

For Gradle projects, add this to your build.gradle:

// build.gradle
dependencies {
    testImplementation 'org.mockito:mockito-core:5.12.0'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0'
}

Basic Mocking Concepts

Before diving into code, understand these fundamental Mockito concepts:

Creating Mocks and Stubs

Creating a Mock Object

There are two primary ways to create a mock in Mockito. The first uses the static mock() method, and the second uses the @Mock annotation combined with a JUnit extension. Let's start with a simple example. Suppose you have a UserRepository interface and a UserService class that depends on it:

// UserRepository.java
public interface UserRepository {
    User findById(Long id);
    void save(User user);
    boolean delete(Long id);
}

// UserService.java
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id);
    }

    public void createUser(User user) {
        if (user.getName() == null || user.getName().isEmpty()) {
            throw new IllegalArgumentException("User name cannot be empty");
        }
        userRepository.save(user);
    }
}

Here is how you create a mock using the mock() method and stub its behavior:

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

public class UserServiceTest {

    @Test
    void testGetUserById() {
        // Arrange: Create a mock UserRepository
        UserRepository mockRepository = mock(UserRepository.class);

        // Create a UserService with the mock dependency
        UserService userService = new UserService(mockRepository);

        // Create a sample user to return from the stub
        User expectedUser = new User(1L, "Alice", "alice@example.com");

        // Stub the findById method to return the expected user when called with ID 1
        when(mockRepository.findById(1L)).thenReturn(expectedUser);

        // Act: Call the method under test
        User actualUser = userService.getUserById(1L);

        // Assert: Verify the returned user matches the expected user
        assertEquals(expectedUser, actualUser);
        assertEquals("Alice", actualUser.getName());
    }
}

Using @Mock Annotation with @ExtendWith

The annotation-based approach reduces boilerplate code and is preferred for larger test classes. Use the @ExtendWith(MockitoExtension.class) annotation to enable automatic mock initialization:

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class UserServiceAnnotationTest {

    @Mock
    private UserRepository userRepository;

    @Test
    void testGetUserById() {
        // The mock is automatically initialized by MockitoExtension
        UserService userService = new UserService(userRepository);

        User expectedUser = new User(1L, "Alice", "alice@example.com");
        when(userRepository.findById(1L)).thenReturn(expectedUser);

        User actualUser = userService.getUserById(1L);

        assertEquals(expectedUser, actualUser);
    }
}

Stubbing with Different Return Values

Mockito provides several ways to define what a stub should return. You can return a fixed value, compute a dynamic value using a lambda, return values sequentially on consecutive calls, or throw an exception:

import static org.mockito.Mockito.when;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.doReturn;

// Stub to return a fixed value
when(mockRepository.findById(1L)).thenReturn(new User(1L, "Alice", "alice@example.com"));

// Stub to return different values on consecutive calls
when(mockRepository.findById(1L))
    .thenReturn(new User(1L, "Alice", "alice@example.com"))
    .thenReturn(new User(1L, "Alice Updated", "alice_new@example.com"))
    .thenThrow(new RuntimeException("Database connection lost"));

// Stub using a lambda (Answer) for dynamic behavior
when(mockRepository.findById(anyLong())).thenAnswer(invocation -> {
    Long id = invocation.getArgument(0);
    return new User(id, "User_" + id, "user" + id + "@example.com");
});

// For void methods, use doThrow or doNothing
doThrow(new IllegalArgumentException("Invalid user")).when(mockRepository).save(null);
doReturn(null).when(mockRepository).save(any(User.class));

The thenReturn method is used for non-void methods, while doReturn, doThrow, and doNothing are used for void methods or when you need to stub methods on spies without invoking the real implementation. Note that doReturn does not type-check the return value at compile time, so use when().thenReturn() when possible for type safety.

Verifying Interactions

Verification allows you to assert that specific methods were called on your mocks during the test. This is crucial for testing side effects, such as ensuring that a service calls the repository's save method after processing data.

Basic Verification

import static org.mockito.Mockito.verify;

@Test
void testCreateUserSavesToRepository() {
    UserService userService = new UserService(userRepository);
    User newUser = new User(null, "Bob", "bob@example.com");

    userService.createUser(newUser);

    // Verify that userRepository.save() was called exactly once with the expected user
    verify(userRepository).save(newUser);
}

Verifying the Number of Invocations

Mockito provides additional verification modes to assert exactly how many times a method was called:

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.atLeastOnce;

// Verify exactly 3 invocations
verify(mockRepository, times(3)).findById(1L);

// Verify at least 2 invocations
verify(mockRepository, atLeast(2)).findById(1L);

// Verify at most 5 invocations
verify(mockRepository, atMost(5)).findById(1L);

// Verify at least once (default for verify without argument)
verify(mockRepository, atLeastOnce()).findById(1L);

// Verify that a method was never called
verify(mockRepository, never()).delete(1L);

// Verify no other interactions occurred on the mock beyond those already verified
verifyNoMoreInteractions(mockRepository);

// Verify no interactions at all occurred on the mock
verifyNoInteractions(mockRepository);

Verification Order

Sometimes the order of method calls matters. Mockito can verify that interactions happened in a specific sequence:

import static org.mockito.Mockito.inOrder;
import org.mockito.InOrder;

@Test
void testOrderOfOperations() {
    UserService userService = new UserService(userRepository);
    User user = new User(1L, "Charlie", "charlie@example.com");

    userService.createUser(user);
    userService.getUserById(1L);

    // Create an InOrder verifier for the mock
    InOrder inOrder = inOrder(userRepository);

    // Verify that save was called before findById
    inOrder.verify(userRepository).save(user);
    inOrder.verify(userRepository).findById(1L);
}

Argument Matchers

Argument matchers allow you to stub or verify method calls without specifying exact argument values. They are essential when you want to match a range of arguments or when exact equality is impractical or impossible.

Built-in Matchers

Mockito provides a rich set of built-in matchers in the org.mockito.ArgumentMatchers class:

import static org.mockito.ArgumentMatchers.*;

// Match any object of a given type (including null)
when(mockRepository.findById(any(Long.class))).thenReturn(someUser);

// Match any String (including null)
when(mockRepository.findByName(any(String.class))).thenReturn(userList);

// Match any non-null instance of a type
when(mockRepository.findById(anyLong())).thenReturn(someUser);  // Primitive wrapper
when(mockRepository.findByName(notNull())).thenReturn(userList);

// Match using a custom predicate or lambda
when(mockRepository.findByName(argThat(name -> name.startsWith("A"))))
    .thenReturn(usersStartingWithA);

// Match null values explicitly
when(mockRepository.findByName(isNull())).thenThrow(new IllegalArgumentException());

// Match specific values with eq() (required when using any matcher in the same call)
when(mockRepository.update(eq(1L), any(User.class))).thenReturn(true);

// Numeric matchers
when(mockRepository.getUsersByAge(gt(18))).thenReturn(adultUsers);    // greater than
when(mockRepository.getUsersByAge(geq(18))).thenReturn(adultUsers);   // greater or equal
when(mockRepository.getUsersByAge(lt(65))).thenReturn(nonSeniorUsers); // less than
when(mockRepository.getUsersByAge(leq(65))).thenReturn(nonSeniorUsers); // less or equal

// String matchers
when(mockRepository.findByName(startsWith("A"))).thenReturn(aNames);
when(mockRepository.findByName(endsWith("son"))).thenReturn(sonNames);
when(mockRepository.findByName(contains("middle"))).thenReturn(middleNames);
when(mockRepository.findByName(matches("^[A-Z][a-z]+$"))).thenReturn(capitalizedNames);

// Collection matchers
when(mockRepository.findByIdIn(argThat(list -> list.size() > 0))).thenReturn(users);

Important Rule: All-or-Nothing Matchers

If you use an argument matcher for one parameter in a stubbing or verification call, you must use matchers for all parameters. You cannot mix raw values and matchers. If you need to match one parameter broadly but use an exact value for another, wrap the exact value with eq():

// This will throw an exception - mixing raw value and matcher
// when(mockRepository.update(1L, any(User.class))).thenReturn(true);  // INVALID!

// Correct: use eq() for the exact value
when(mockRepository.update(eq(1L), any(User.class))).thenReturn(true);  // VALID

Custom Argument Matchers

You can create custom argument matchers by implementing the ArgumentMatcher interface. This is useful for domain-specific assertions:

import org.mockito.ArgumentMatcher;

// Define a custom matcher for User objects with a specific domain
public class UserWithDomain implements ArgumentMatcher {
    private final String domain;

    public UserWithDomain(String domain) {
        this.domain = domain;
    }

    @Override
    public boolean matches(User user) {
        return user != null && user.getEmail() != null 
               && user.getEmail().endsWith("@" + domain);
    }

    @Override
    public String toString() {
        return "User with email domain @" + domain;
    }
}

// Usage in a test
@Test
void testCustomMatcher() {
    when(mockRepository.save(argThat(new UserWithDomain("example.com")))).thenReturn(true);

    User user = new User(1L, "Alice", "alice@example.com");
    userService.createUser(user);

    verify(mockRepository).save(argThat(new UserWithDomain("example.com")));
}

Argument Captors

Argument captors allow you to capture the arguments passed to mock methods for detailed inspection. This is particularly useful when the argument is created or modified inside the method under test, and you need to assert on its internal state.

import static org.mockito.Mockito.verify;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;

@ExtendWith(MockitoExtension.class)
public class ArgumentCaptorTest {

    @Mock
    private UserRepository userRepository;

    @Captor
    private ArgumentCaptor userCaptor;

    @Test
    void testUserFieldsWhenSaving() {
        UserService userService = new UserService(userRepository);
        userService.createUser(new User(null, "Diana", "diana@example.com"));

        // Capture the User argument passed to save()
        verify(userRepository).save(userCaptor.capture());

        // Inspect the captured value
        User capturedUser = userCaptor.getValue();
        assertEquals("Diana", capturedUser.getName());
        assertEquals("diana@example.com", capturedUser.getEmail());
        assertNull(capturedUser.getId()); // ID should be null before saving
    }

    @Test
    void testMultipleCaptures() {
        UserService userService = new UserService(userRepository);
        userService.createUser(new User(null, "User1", "user1@example.com"));
        userService.createUser(new User(null, "User2", "user2@example.com"));
        userService.createUser(new User(null, "User3", "user3@example.com"));

        // Capture all values
        verify(userRepository, times(3)).save(userCaptor.capture());

        List allCapturedUsers = userCaptor.getAllValues();
        assertEquals(3, allCapturedUsers.size());
        assertEquals("User1", allCapturedUsers.get(0).getName());
        assertEquals("User2", allCapturedUsers.get(1).getName());
        assertEquals("User3", allCapturedUsers.get(2).getName());
    }
}

Spying on Real Objects

A spy wraps a real object and allows you to stub specific methods while delegating to the real implementation for all others. Spies are useful when you want to test a class that has some methods you want to mock and others you want to execute normally. However, spies should be used sparingly—they often indicate that the class under test could benefit from better separation of concerns.

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;

@Test
void testSpyExample() {
    // Create a real object
    UserRepository realRepository = new InMemoryUserRepository();

    // Wrap it in a spy
    UserRepository spyRepository = spy(realRepository);

    // Stub only the findById method to return a controlled value
    doReturn(new User(1L, "Mocked User", "mocked@example.com"))
        .when(spyRepository).findById(1L);

    // All other methods delegate to the real implementation
    UserService userService = new UserService(spyRepository);

    // This call uses the stub
    User user = userService.getUserById(1L);
    assertEquals("Mocked User", user.getName());

    // This call uses the real implementation
    userService.createUser(new User(null, "Real User", "real@example.com"));
    // The real save method was actually invoked
}

Critical rule for spies: Always use doReturn().when() syntax instead of when().thenReturn() when stubbing spy methods. The when().thenReturn() syntax actually invokes the real method before stubbing it, which can cause unwanted side effects. The doReturn() syntax avoids this by never calling the real method during stub setup:

// WRONG for spies - calls the real method during stub setup!
// when(spyRepository.findById(1L)).thenReturn(mockUser);

// CORRECT for spies - does not invoke the real method
doReturn(mockUser).when(spyRepository).findById(1L);

Mocking Void Methods

Void methods require a different stubbing syntax because they don't return a value. Mockito provides doNothing(), doThrow(), and doAnswer() for this purpose:

import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.doAnswer;

@Test
void testVoidMethodStubs() {
    // Stub a void method to do nothing (this is the default behavior for mocks)
    doNothing().when(mockRepository).save(any(User.class));

    // Stub a void method to throw an exception
    doThrow(new RuntimeException("Database error")).when(mockRepository).delete(1L);

    // Stub a void method to perform a custom action using doAnswer
    doAnswer(invocation -> {
        User user = invocation.getArgument(0);
        // Simulate setting an ID as if the database generated it
        user.setId(42L);
        return null; // Void methods must return null in doAnswer
    }).when(mockRepository).save(any(User.class));

    // Now test: when save is called, the user's ID gets set
    User user = new User(null, "Eve", "eve@example.com");
    userService.createUser(user);
    assertEquals(42L, user.getId());
}

Mocking Static Methods

Mockito 3.x and later support mocking static methods via the mockito-inline artifact (which is now the default in mockito-core 5.x). This is particularly useful for testing code that depends on static utility classes or singletons. To mock static methods, use the MockedStatic interface within a try-with-resources block to ensure the mock is cleaned up after the test:

import static org.mockito.Mockito.mockStatic;
import org.mockito.MockedStatic;

// Suppose you have a static utility class
public class DateTimeUtils {
    public static LocalDate getCurrentDate() {
        return LocalDate.now();
    }
}

// Class under test that uses the static method
public class ReportService {
    public String generateReport() {
        LocalDate today = DateTimeUtils.getCurrentDate();
        return "Report generated on: " + today.toString();
    }
}

// Test class
@Test
void testStaticMethodMocking() {
    // Use try-with-resources to scope the static mock
    try (MockedStatic mockedStatic = mockStatic(DateTimeUtils.class)) {
        // Stub the static method to return a fixed date
        mockedStatic.when(DateTimeUtils::getCurrentDate)
                    .thenReturn(LocalDate.of(2024, 12, 25));

        ReportService reportService = new ReportService();
        String report = reportService.generateReport();

        assertEquals("Report generated on: 2024-12-25", report);

        // Verify the static method was called
        mockedStatic.verify(DateTimeUtils::getCurrentDate);
    }
    // After the try block, the static mock is automatically closed and reset
}

You can also stub static methods to throw exceptions or return different values on consecutive calls:

try (MockedStatic mockedStatic = mockStatic(DateTimeUtils.class)) {
    mockedStatic.when(DateTimeUtils::getCurrentDate)
                .thenReturn(LocalDate.of(2024, 1, 1))
                .thenReturn(LocalDate.of(2024, 1, 2))
                .thenThrow(new RuntimeException("Time service unavailable"));

    // First call returns Jan 1
    LocalDate date1 = DateTimeUtils.getCurrentDate();
    // Second call returns Jan 2
    LocalDate date2 = DateTimeUtils.getCurrentDate();
    // Third call throws RuntimeException
    // LocalDate date3 = DateTimeUtils.getCurrentDate(); // throws exception
}

Mocking Final Classes and Methods

Historically, Mockito could not mock final classes or methods because it relied on dynamic proxy generation. Since Mockito 2.x and the inline mock maker (now default in 5.x), you can mock final classes and methods directly. This is essential for testing code that depends on final JDK classes or libraries that mark their classes as final:

// A final class that you want to mock
public final class PaymentGateway {
    public final boolean processPayment(String accountId, BigDecimal amount) {
        // Real implementation calls external payment processor
        return callExternalService(accountId, amount);
    }
    private boolean callExternalService(String accountId, BigDecimal amount) {
        // Complex external call
        return true;
    }
}

// Class under test
public class OrderService {
    private final PaymentGateway paymentGateway;

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

    public String placeOrder(String accountId, BigDecimal amount) {
        boolean success = paymentGateway.processPayment(accountId, amount);
        return success ? "Order placed" : "Payment failed";
    }
}

// Test class - this works with Mockito 5.x out of the box
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {

    @Mock
    private PaymentGateway paymentGateway; // Final class mock works!

    @Test
    void testPlaceOrderSuccess() {
        OrderService orderService = new OrderService(paymentGateway);

        // Stub the final method
        when(paymentGateway.processPayment(eq("account123"), any(BigDecimal.class)))
            .thenReturn(true);

        String result = orderService.placeOrder("account123", new BigDecimal("99.99"));

        assertEquals("Order placed", result);
        verify(paymentGateway).processPayment("account123", new BigDecimal("99.99"));
    }
}

If you're using an older version of Mockito or need to explicitly enable inline mock maker, create a file at src/test/resources/mockito.extensions with the content:

mock-maker=inline

Advanced Features

BDDMockito Style

Mockito provides a BDD (Behavior-Driven Development) flavor through the BDDMockito class. This style uses given() instead of when() and then() / willReturn() instead of thenReturn(). It also uses should() for verification. This aligns better with the given-when-then structure of BDD tests:

import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willReturn;

@Test
void testWithBDDStyle() {
    // Given (arrange)
    User expectedUser = new User(1L, "Alice", "alice@example.com");
    given(userRepository.findById(1L)).willReturn(expectedUser);
    UserService userService = new UserService(userRepository);

    // When (act)
    User actualUser = userService.getUserById(1L);

    // Then (assert)
    assertEquals(expectedUser, actualUser);
    then(userRepository).should().findById(1L);
    then(userRepository).shouldHaveNoMoreInteractions();
}

Resetting Mocks

While generally discouraged (it's better to write independent tests), you can reset mocks using reset():

import static org.mockito.Mockito.reset;

@Test
void testResetMock() {
    when(userRepository.findById(1L)).thenReturn(new User(1L, "Alice", "alice@example.com"));
    UserService userService = new UserService(userRepository);
    userService.getUserById(1L);

    // Reset clears all stubs and verifications on the mock
    reset(userRepository);

    // Now the mock behaves like a fresh mock with no stubs
    User user = userService.getUserById(1L);
    assertNull(user); // Default return value for unstubbed method
}

Mocking with Custom Answers

The Answer interface provides the most flexible way to define mock behavior. You can access invocation details, modify arguments, and implement stateful mocks:

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

// Custom Answer that delegates to a real in-memory map
public class RepositoryAnswer implements Answer {
    private final Map storage = new HashMap<>();

    @Override
    public User answer(InvocationOnMock invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        if ("findById".equals(methodName)) {
            Long id = invocation.getArgument(0);
            return storage.get(id);
        } else if ("save".equals(methodName)) {
            User user = invocation.getArgument(0);
            storage.put(user.getId(), user);
            return null;
        }
        return null;
    }
}

@Test
void testCustomAnswer() {
    UserRepository mockRepository = mock(UserRepository.class, new RepositoryAnswer());
    UserService userService = new UserService(mockRepository);

    // The mock behaves like a real in-memory repository
    userService.createUser(new User(1L, "Alice", "alice@example.com"));
    User retrieved = userService.getUserById(1L);

    assertEquals("Alice", retrieved.getName());
}

Deep Stubs

Deep stubs allow you to automatically mock chained method calls without explicitly stubbing each intermediate object. Use with caution as they can hide design problems like violations of the Law of Demeter:

// Without deep stubs, you must mock each level
Address address = mock(Address.class);
when(user.getAddress()).thenReturn(address);
when(address.getCity()).thenReturn("New York");

// With deep stubs (RETURNS_DEEP_STUBS), chained calls return mocks automatically
User user = mock(User.class, RETURNS_DEEP_STUBS);
when(user.getAddress().getCity()).thenReturn("New York");
// The intermediate getAddress() automatically returns a mock

// Even deeper chains
when(user.getCompany().getDepartment().getName()).thenReturn("Engineering");
// Both getCompany() and getDepartment() return mocks automatically

Best Practices

1. Mock Only What You Own

Mock types that you control—interfaces and classes within your own codebase. Avoid mocking types from third-party libraries when possible. Instead, wrap third-party interactions in your own adapter or interface, and mock that adapter. This protects your tests from changes in external library APIs.

2. Prefer Constructor Injection

Use constructor injection to provide dependencies to your classes. This makes it trivial to inject mocks in tests without relying on reflection or Spring-specific test utilities. Field injection and setter injection make testing more difficult and obscure dependencies.

3. Keep Stubs Minimal and Specific

Stub only the methods that your test actually needs. Over-stubbing with broad matchers like any() can hide bugs where the code uses arguments differently than expected. Be as specific as possible in your stubs:

// Prefer this - specific stub
when(userRepository.findById(1L)).thenReturn(specificUser);

// Over this - overly broad stub that may hide bugs
when(userRepository.findById(anyLong())).thenReturn(genericUser);

4. Verify Only Important Interactions

Not every method call needs verification. Focus on verifying critical side effects and interactions that are essential to the contract of the method under test. Over-verification leads to brittle tests that break for irrelevant implementation changes.

5. One Mock Per Test, When Possible

A test that requires multiple mocks often indicates that the method under test is doing too much (violating the Single Responsibility Principle). Consider whether the code could be refactored. When multiple mocks are necessary, ensure each mock serves a clear purpose in the test scenario.

6. Avoid Mocking Value Objects

Value objects like User, Address, Money, or Email should be created as real instances, not mocks. These objects are data carriers with no significant behavior to mock. Using real value objects makes tests more readable and avoids unnecessary mocking complexity.

7. Use Spies Sparingly

Spies indicate a design where you want to test a class but mock part of it. This often suggests that the class has multiple responsibilities that should be separated. Prefer refactoring the class into smaller, focused components that can be tested independently with mocks.

8. Clean Up Static Mocks

Always use try-with-resources when mocking static methods to ensure the mock is properly closed and the original behavior restored. Failing to do so can cause static mocks to leak into other tests, leading to confusing and non-deterministic test failures.

9. Write Readable Test Names and Structure

Follow the arrange-act-assert pattern clearly. Use the <

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles