Learnitweb

Replacing Stubs with Mocks for Cleaner Unit Tests

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

FeatureStubMock (Mockito)
Manual class creationYesNo
Interface maintenanceMust update all stubsNot required
Test readabilityLowerHigher
ConfigurationStatic codeDynamic, per test case
ScalabilityPoorExcellent

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());
    }
}