In the previous step, we explored how to write unit tests using stub implementations. While stubs work for small scenarios, we saw that they become hard to manage and maintain as the complexity of tests grows.
1. What is Mockito?
Mockito is a mocking framework for Java. Mockito allows convenient creation of substitutes of real objects for testing purposes. Enjoy clean tests with mock objects, improved TDD experience and beautiful mocking API.
There is a bit of confusion around the vocabulary. Technically speaking, Mockito is a Test Spy framework. Usually developers use Mockito instead of a mocking framework. Test Spy framework allows to verify behaviour (like mocks) and stub methods (like good old hand-crafted stubs).
To promote simple test code that hopefully pushes the developer to write simple and clean application code. I wrote this paragraph long before version 1.5. Mockito is still quite lean but the number of features increased because many users found valid cases for them.
2. Refactor Test to Use a Mock Instead of Stub
Let’s refactor the stub-based test to use a mock object for SomeDataService
.
package com.learnitweb.unittesting.business; import com.learnitweb.unittesting.data.SomeDataService; import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; public class SomeBusinessMockTest { @Test void calculateSumUsingDataService_basic() { // Step 1: Create mock object SomeDataService mockDataService = mock(SomeDataService.class); // Step 2: Define behavior of the mock when(mockDataService.retrieveAllData()).thenReturn(new int[]{1, 2, 3}); // Step 3: Inject mock into business class SomeBusinessImpl business = new SomeBusinessImpl(); business.setDataService(mockDataService); // Step 4: Call method and assert result int result = business.calculateSumUsingDataService(); assertEquals(6, result); } }
3. Explanation of Mockito Usage
3.1 Creating a Mock
SomeDataService mock = mock(SomeDataService.class);
Creates a mock object of the interface. No real implementation is needed.
3.2 Defining Behavior
when(mock.retrieveAllData()).thenReturn(new int[]{1, 2, 3});
Tells the mock: If someone calls retrieveAllData()
, return [1, 2, 3]
.
3.3 Injecting the Mock
business.setDataService(mock);
Instead of using a stub, we inject this mock directly into the business logic.
4. Comparison: Stub vs Mock
Feature | Stub | Mock (Mockito) |
Manual class creation | Yes | No |
Interface maintenance | Must update all stubs | Not required |
Test readability | Lower | Higher |
Configuration | Static code | Dynamic, per test case |
Scalability | Poor | Excellent |
5. Add More Mock-Based Tests
You can now replace the remaining stub-based tests with mocks. Here’s how:
Empty Data Test
@Test void calculateSumUsingDataService_emptyArray() { SomeDataService mock = mock(SomeDataService.class); when(mock.retrieveAllData()).thenReturn(new int[]{}); SomeBusinessImpl business = new SomeBusinessImpl(); business.setDataService(mock); assertEquals(0, business.calculateSumUsingDataService()); }
Single Value Test
@Test void calculateSumUsingDataService_oneValue() { SomeDataService mock = mock(SomeDataService.class); when(mock.retrieveAllData()).thenReturn(new int[]{5}); SomeBusinessImpl business = new SomeBusinessImpl(); business.setDataService(mock); assertEquals(5, business.calculateSumUsingDataService()); }
6. @BeforeEach
Before Refactoring:
@Test void calculateSumUsingDataService_basic() { SomeDataService mock = mock(SomeDataService.class); when(mock.retrieveAllData()).thenReturn(new int[]{1, 2, 3}); SomeBusinessImpl business = new SomeBusinessImpl(); business.setDataService(mock); assertEquals(6, business.calculateSumUsingDataService()); } @Test void calculateSumUsingDataService_emptyArray() { SomeDataService mock = mock(SomeDataService.class); when(mock.retrieveAllData()).thenReturn(new int[]{}); SomeBusinessImpl business = new SomeBusinessImpl(); business.setDataService(mock); assertEquals(0, business.calculateSumUsingDataService()); } @Test void calculateSumUsingDataService_oneValue() { SomeDataService mock = mock(SomeDataService.class); when(mock.retrieveAllData()).thenReturn(new int[]{5}); SomeBusinessImpl business = new SomeBusinessImpl(); business.setDataService(mock); assertEquals(5, business.calculateSumUsingDataService()); }
Problem:
- Repeated code for:
- Creating the mock
- Creating
SomeBusinessImpl
- Injecting the mock
- Reduced readability and maintainability
6.1 Step-by-Step Refactoring
Use JUnit 5’s @BeforeEach
annotation to run setup code before each test method.
package com.in28minutes.unittesting.business; import com.in28minutes.unittesting.data.SomeDataService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; public class SomeBusinessMockTest { SomeDataService mockDataService; SomeBusinessImpl business; @BeforeEach public void setup() { mockDataService = mock(SomeDataService.class); business = new SomeBusinessImpl(); business.setDataService(mockDataService); } @Test void calculateSumUsingDataService_basic() { when(mockDataService.retrieveAllData()).thenReturn(new int[]{1, 2, 3}); assertEquals(6, business.calculateSumUsingDataService()); } @Test void calculateSumUsingDataService_emptyArray() { when(mockDataService.retrieveAllData()).thenReturn(new int[]{}); assertEquals(0, business.calculateSumUsingDataService()); } @Test void calculateSumUsingDataService_oneValue() { when(mockDataService.retrieveAllData()).thenReturn(new int[]{5}); assertEquals(5, business.calculateSumUsingDataService()); } }