Mocking #

Good unit tests are fast and isolated — they don’t touch the database, don’t call external APIs, and don’t depend on the system clock or environment variables. But how do you test a ProductService that calls a ProductRepository to fetch data from a database? The answer: replace the real ProductRepository with a mock object that you fully control. You decide: “if findById(1) is called, return this product.” The code under test doesn’t know the difference — it still interacts with the same interface. With mocking, you can test every branch of ProductService’s logic without starting a single database. In Java, Mockito is the most widely used mocking library — expressive, well integrated with JUnit 5, and supporting almost every scenario you need.

Basic Concepts #

Before writing code, there are three types of fake objects that often get confused:

TypeDefault BehaviorWhen to Use
MockAll methods return default values (null, 0, false) until stubbedWhen you need full control and interaction verification
StubA simple object returning fixed valuesWhen you only need input control, don’t care about interactions
SpyWraps a real object; unstubbed methods call the original implementationWhen you want to override part of an existing object’s behavior
flowchart LR
    A["Test"] -->|"calls"| B["Class under test\n(ProductService)"]
    B -->|"calls"| C["Mock/Stub/Spy\n(fake ProductRepository)"]
    C -->|"return predetermined\nvalues"| B
    B -->|"return result"| A
    A -->|"verify()"| C

Mockito #

Mockito is the de facto standard mocking library in the Java ecosystem. It works by creating subclasses or proxies of the mocked class/interface using bytecode manipulation.

Dependencies #

<!-- Maven — mockito-junit-jupiter already includes mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>
// Gradle
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'

Creating Mocks — Two Ways #

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

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

// The interface and class under test
interface ProductRepository {
    Product findById(Long id);
    List<Product> findAll();
    Product save(Product product);
    void delete(Long id);
}

class ProductService {
    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public Product getProduct(Long id) {
        Product p = repository.findById(id);
        if (p == null) throw new RuntimeException("Product not found: " + id);
        return p;
    }

    public Product createProduct(String name, double price) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name is required");
        Product created = new Product(null, name, price);
        return repository.save(created);
    }
}

// Way 1: @ExtendWith + @Mock (recommended)
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    @Mock
    ProductRepository repository;    // Mockito creates the mock automatically

    @InjectMocks
    ProductService service;          // Mockito injects the mock into the constructor/field

    @Test
    void getProduct_returnsExistingProduct() {
        // Arrange — define the mock's behavior
        Product laptop = new Product(1L, "Laptop", 12_000_000);
        when(repository.findById(1L)).thenReturn(laptop);

        // Act
        Product result = service.getProduct(1L);

        // Assert
        assertEquals("Laptop", result.name());
        assertEquals(12_000_000, result.price());
    }
}

// Way 2: Mockito.mock() manually (for tests without @ExtendWith)
class ProductServiceTestManual {

    @Test
    void getProductManual() {
        ProductRepository mockRepo = mock(ProductRepository.class);
        ProductService service = new ProductService(mockRepo);

        when(mockRepo.findById(1L)).thenReturn(new Product(1L, "Mouse", 150_000));
        assertEquals("Mouse", service.getProduct(1L).name());
    }
}

Stubbing — Defining Mock Behavior #

@ExtendWith(MockitoExtension.class)
class StubbingTest {

    @Mock ProductRepository repository;

    @Test
    void variousStubbingWays() {
        Product laptop = new Product(1L, "Laptop", 12_000_000);

        // thenReturn: return a fixed value
        when(repository.findById(1L)).thenReturn(laptop);

        // thenReturn with multiple values: first call returns laptop, next returns null
        when(repository.findById(2L))
            .thenReturn(laptop)
            .thenReturn(null);

        // thenThrow: throw an exception
        when(repository.findById(999L))
            .thenThrow(new RuntimeException("Database connection failed"));

        // thenAnswer: custom logic based on arguments
        when(repository.findById(anyLong()))
            .thenAnswer(invocation -> {
                Long id = invocation.getArgument(0);
                return id > 0 ? new Product(id, "Product-" + id, id * 1000.0) : null;
            });

        // doReturn (for void methods or spies)
        doNothing().when(repository).delete(anyLong());
        doThrow(new RuntimeException("Delete failed")).when(repository).delete(-1L);

        // Stubbing for void methods
        // (can't use when().thenXxx() for void)
        doAnswer(invocation -> {
            System.out.println("Deleting ID: " + invocation.getArgument(0));
            return null;
        }).when(repository).delete(anyLong());
    }
}

Argument Matchers #

Argument matchers enable more flexible stubbing and verification — you don’t need to specify exact argument values.

import static org.mockito.ArgumentMatchers.*;

@Test
void argumentMatchers() {
    Product laptop = new Product(1L, "Laptop", 12_000_000);

    // any(): matches any argument of the given type
    when(repository.findById(any(Long.class))).thenReturn(laptop);
    when(repository.findById(anyLong())).thenReturn(laptop);   // shorthand

    // eq(): matches an exact value (useful when combining with other matchers)
    when(repository.findById(eq(1L))).thenReturn(laptop);

    // String matchers
    when(repository.findByName(anyString())).thenReturn(List.of(laptop));
    when(repository.findByName(startsWith("Lap"))).thenReturn(List.of(laptop));
    when(repository.findByName(contains("top"))).thenReturn(List.of(laptop));

    // Collection matchers
    when(repository.findAllById(anyList())).thenReturn(List.of(laptop));

    // isNull() and isNotNull()
    when(repository.save(isNull())).thenThrow(new IllegalArgumentException());
    when(repository.save(isNotNull())).thenReturn(laptop);

    // Combination — WARNING: if you use a matcher on one argument,
    // ALL arguments must use matchers
    // when(repo.findByNameAndPrice("Laptop", anyDouble())).thenReturn(...); // ✗ error
    when(repository.findByNameAndPrice(eq("Laptop"), anyDouble())).thenReturn(List.of(laptop)); // ✓
}

Interaction Verification #

After running the code under test, you can verify how the mock was called — how many times, with what arguments, in what order.

Basic Verification #

@Test
void verifyInteractions() {
    Product laptop = new Product(1L, "Laptop", 12_000_000);
    when(repository.findById(1L)).thenReturn(laptop);

    service.getProduct(1L);

    // Verify it was called exactly once
    verify(repository).findById(1L);
    verify(repository, times(1)).findById(1L);     // explicit

    // Verify call counts
    verify(repository, times(3)).findById(anyLong()); // called 3 times
    verify(repository, atLeastOnce()).findById(1L);    // at least once
    verify(repository, atLeast(2)).findById(anyLong()); // at least 2 times
    verify(repository, atMost(5)).findById(anyLong());  // at most 5 times
    verify(repository, never()).delete(anyLong());       // never called

    // Verify no other interactions beyond those verified
    verifyNoMoreInteractions(repository);

    // Verify there were no interactions at all
    verifyNoInteractions(repository);
}

Verifying Call Order #

import org.mockito.InOrder;

@Test
void verifyOrder() {
    when(repository.findById(1L)).thenReturn(new Product(1L, "Laptop", 12_000_000));

    // Run several operations
    service.getProduct(1L);
    service.getProduct(1L);

    // Make sure they're called in this order
    InOrder order = inOrder(repository);
    order.verify(repository).findById(1L);
    order.verify(repository).findById(1L);
}

ArgumentCaptor — Capturing Arguments #

ArgumentCaptor is very useful when you need to verify values sent to a mock, especially when the object is created inside the method under test and you can’t access it directly.

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;

@ExtendWith(MockitoExtension.class)
class CaptorTest {

    @Mock ProductRepository repository;
    @InjectMocks ProductService service;

    @Captor ArgumentCaptor<Product> productCaptor;

    @Test
    void createProduct_savesWithCorrectData() {
        Product saved = new Product(1L, "Monitor", 3_500_000);
        when(repository.save(any())).thenReturn(saved);

        service.createProduct("Monitor", 3_500_000);

        // Capture the argument sent to repository.save()
        verify(repository).save(productCaptor.capture());
        Product sent = productCaptor.getValue();

        // Verify the contents of the sent object
        assertEquals("Monitor", sent.name());
        assertEquals(3_500_000, sent.price());
        assertNull(sent.id()); // no ID from the DB yet
    }

    @Test
    void saveManyProducts_allCaptured() {
        when(repository.save(any())).thenAnswer(i -> i.getArgument(0));

        service.createProduct("Product A", 1000);
        service.createProduct("Product B", 2000);
        service.createProduct("Product C", 3000);

        verify(repository, times(3)).save(productCaptor.capture());
        List<Product> all = productCaptor.getAllValues();

        assertEquals(3, all.size());
        assertEquals("Product A", all.get(0).name());
        assertEquals("Product C", all.get(2).name());
    }
}

Spies — Wrapping Real Objects #

A spy fits when you want to test most of an object’s real behavior but need to override a few specific methods.

Creating and Using a Spy #

import org.mockito.Spy;

@ExtendWith(MockitoExtension.class)
class SpyTest {

    // A spy wraps a real object (unlike a mock, which is purely fake)
    @Spy
    List<String> list = new ArrayList<>();

    @Test
    void spyUsesTheRealImplementation() {
        // Unstubbed methods are called normally
        list.add("Apple");
        list.add("Mango");

        assertEquals(2, list.size()); // the real size

        // Stub only specific methods
        doReturn(100).when(list).size();
        assertEquals(100, list.size()); // now returns 100

        // But the list contents are still real
        assertTrue(list.contains("Apple"));
    }

    @Test
    void spyFromAnExistingObject() {
        // How to create a spy from an existing object
        EmailService emailService = spy(new EmailServiceImpl());

        // Override only the send method so it doesn't actually send emails
        doNothing().when(emailService).sendEmail(anyString(), anyString());

        // Other methods (like validation) still run for real
        assertTrue(emailService.isValidEmail("[email protected]"));

        // Call a method that uses the emailService
        NotificationService notification = new NotificationService(emailService);
        notification.sendRegistrationNotification("[email protected]");

        // Verify sendEmail was called with the right arguments
        verify(emailService).sendEmail(eq("[email protected]"), contains("Welcome"));
    }
}

Care When Stubbing Spies #

@Test
void spyStubbingCaution() {
    List<String> list = spy(new ArrayList<>());

    // ANTI-PATTERN: when().thenReturn() on a spy calls the real method first
    // An empty list → get(0) throws IndexOutOfBoundsException before thenReturn runs!
    // when(list.get(0)).thenReturn("zero"); // ✗ throws an exception

    // CORRECT: use doReturn().when() for spies — doesn't call the real method
    doReturn("zero").when(list).get(0); // ✓ safe
}

Mocking Static Methods #

Since Mockito 3.4.0+, static methods can be mocked using MockedStatic. This is useful for mocking LocalDate.now(), UUID.randomUUID(), or other utility classes.

Mocking Static Methods #

import org.mockito.MockedStatic;
import java.time.LocalDate;

@Test
void mockTodayDate() {
    LocalDate fixedDate = LocalDate.of(2025, 8, 17);

    // try-with-resources: the static mock is only active inside this block
    try (MockedStatic<LocalDate> mockedDate = mockStatic(LocalDate.class)) {
        mockedDate.when(LocalDate::now).thenReturn(fixedDate);

        // Code calling LocalDate.now() inside this block
        // will get 2025-08-17, not the real date
        LocalDate result = LocalDate.now();
        assertEquals(LocalDate.of(2025, 8, 17), result);

        // The code under test is affected too
        String report = service.createReport(); // createReport() has LocalDate.now() inside
        assertTrue(report.contains("2025-08-17"));
    }

    // Outside the try block: LocalDate.now() is back to normal
}

@Test
void mockUUID() {
    java.util.UUID fixed = java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000");

    try (MockedStatic<java.util.UUID> mockedUUID = mockStatic(java.util.UUID.class)) {
        mockedUUID.when(java.util.UUID::randomUUID).thenReturn(fixed);

        String id = service.createUniqueId();
        assertEquals("123e4567-e89b-12d3-a456-426614174000", id);
    }
}

EasyMock #

EasyMock is a Mockito alternative with a slightly different approach — it uses the record-replay-verify pattern. You record expectations first, activate with replay(), run the code, then verify with verify().

Dependencies and Usage #

<!-- Maven -->
<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>
import org.easymock.EasyMock;
import static org.easymock.EasyMock.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ProductServiceEasyMockTest {

    @Test
    void getProductWithEasyMock() {
        // 1. Create the mock
        ProductRepository mockRepo = createMock(ProductRepository.class);

        // 2. Record: define expectations
        Product laptop = new Product(1L, "Laptop", 12_000_000);
        expect(mockRepo.findById(1L)).andReturn(laptop);

        // 3. Replay: activate the mock
        replay(mockRepo);

        // 4. Run the code under test
        ProductService service = new ProductService(mockRepo);
        Product result = service.getProduct(1L);

        // 5. Assert
        assertEquals("Laptop", result.name());

        // 6. Verify: make sure all expectations were met
        verify(mockRepo);
    }

    @Test
    void mockThrowsException() {
        ProductRepository mockRepo = createMock(ProductRepository.class);

        // Expectation: findById(999) throws an exception
        expect(mockRepo.findById(999L))
            .andThrow(new RuntimeException("Not found"));
        replay(mockRepo);

        ProductService service = new ProductService(mockRepo);
        assertThrows(RuntimeException.class, () -> service.getProduct(999L));

        verify(mockRepo);
    }
}

EasyMock vs Mockito #

AspectMockitoEasyMock
Stubbing stylewhen(mock.method()).thenReturn(value)expect(mock.method()).andReturn(value)
ActivationNot needed (active immediately)Must call replay()
Verificationverify(mock).method()verify(mock) — all expectations
Default behaviorReturns null/0/falseThrows if called without an expectation
PopularityVery highLower, less actively developed

Mocking Anti-Patterns #

Over-mocking #

// ANTI-PATTERN: mocking too many things, the test doesn't exercise real logic
@Test
void testWithTooManyMocks() {
    when(repository.findById(any())).thenReturn(mock(Product.class));
    when(product.getName()).thenReturn("Laptop");
    when(product.getPrice()).thenReturn(12_000_000.0);
    when(formatter.format(any())).thenReturn("$12,000,000");
    // ... 10 more stubs
    // This test only verifies that mocks were called, not business logic

// CORRECT: mock only external dependencies (I/O, network, DB)
// Use real objects for value objects and internal logic
@Test
void correctTest() {
    Product laptop = new Product(1L, "Laptop", 12_000_000); // real object
    when(repository.findById(1L)).thenReturn(laptop);      // only the DB is mocked
    // ...
}

Testing Implementation Details, Not Behavior #

// ANTI-PATTERN: verifying how the implementation works, not the result
@Test
void testImplementationDetail() {
    service.getProduct(1L);

    // This tests "how" not "what" — fragile if the implementation changes
    verify(repository).findById(1L);
    verify(cache).get("product:1");
    verify(logger).log(any());
    // this test will fail if we refactor the internal call order
}

// CORRECT: focus on externally visible behavior
@Test
void testBehavior() {
    Product laptop = new Product(1L, "Laptop", 12_000_000);
    when(repository.findById(1L)).thenReturn(laptop);

    Product result = service.getProduct(1L); // verify the result, not how it was achieved

    assertEquals("Laptop", result.name());
    // only verify what's truly important by contract
}

Mocking Classes You Don’t Own #

// ANTI-PATTERN: mocking a third-party library directly
HttpClient mockClient = mock(HttpClient.class);
when(mockClient.send(any(), any())).thenReturn(mock(HttpResponse.class));
// This is fragile: the HttpClient API can change, and you're testing your assumptions, not your code

// CORRECT: wrap the third-party library in your own interface
interface HttpGateway {
    String get(String url);
    String post(String url, String body);
}

// Then mock the interface you created
HttpGateway mockGateway = mock(HttpGateway.class);
when(mockGateway.get("https://api.example.com/products/1")).thenReturn("{\"id\":1}");

When to Use Mocking #

MOCK dependencies that:
  ✓ Access databases or external storage
  ✓ Call APIs or network services
  ✓ Depend on time (LocalDate.now(), Instant.now())
  ✓ Access the filesystem or environment variables
  ✓ Are expensive to initialize in tests
  ✓ Are non-deterministic (random, threading)

DON'T mock:
  ✗ Value objects (Product, Address, Money) — use real objects
  ✗ Simple business logic — use the real implementation
  ✗ Third-party libraries directly — wrap them in an interface first
  ✗ Classes whose API you don't control

Choose the mock type:
  → @Mock + @InjectMocks  : most common, for interfaces and classes with injection
  → spy()                 : when you need to override part of a real object's behavior
  → MockedStatic          : for static methods (UUID, LocalDate, etc.)
  → ArgumentCaptor        : when you need to verify the contents of objects sent to mocks

Summary #

  • Mocks isolate the code under test from external dependencies — databases, networks, and filesystems are replaced with objects you fully control, making tests fast and deterministic.
  • @ExtendWith(MockitoExtension.class) + @Mock + @InjectMocks is the standard Mockito pattern with JUnit 5. Mockito automatically creates mocks and injects them into the class under test.
  • when().thenReturn() for stubbing return values, when().thenThrow() to simulate errors, doNothing().when() for void methods.
  • Argument matchers (any(), anyString(), eq(), contains()) make stubbing and verification more flexible when exact arguments aren’t important or aren’t known.
  • verify(mock).method() to ensure an interaction happened. Use times(), never(), atLeastOnce() to control expected call counts.
  • ArgumentCaptor captures arguments sent to a mock — useful when objects are created inside the method under test and you need to verify their contents.
  • Spies wrap real objects — unstubbed methods call the original implementation. Use doReturn().when() (not when().thenReturn()) for spies so the real method isn’t called during stubbing.
  • MockedStatic for mocking static methods inside a try-with-resources block — the mock is only active within that block and automatically revoked afterward.
  • Don’t over-mock — mock only external dependencies. Value objects, simple business logic, and objects without side effects don’t need mocking.

← Previous: Unit Test   Next: Stream →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact