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:
| Type | Default Behavior | When to Use |
|---|---|---|
| Mock | All methods return default values (null, 0, false) until stubbed | When you need full control and interaction verification |
| Stub | A simple object returning fixed values | When you only need input control, don’t care about interactions |
| Spy | Wraps a real object; unstubbed methods call the original implementation | When 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()"| CMockito #
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 #
| Aspect | Mockito | EasyMock |
|---|---|---|
| Stubbing style | when(mock.method()).thenReturn(value) | expect(mock.method()).andReturn(value) |
| Activation | Not needed (active immediately) | Must call replay() |
| Verification | verify(mock).method() | verify(mock) — all expectations |
| Default behavior | Returns null/0/false | Throws if called without an expectation |
| Popularity | Very high | Lower, 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+@InjectMocksis 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. Usetimes(),never(),atLeastOnce()to control expected call counts.ArgumentCaptorcaptures 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()(notwhen().thenReturn()) for spies so the real method isn’t called during stubbing.MockedStaticfor 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.