JUnit 5 and Mockito

Testing is a critical part of software development that ensures code quality, reduces bugs, and facilitates maintainability. In the Java ecosystem, JUnit 5 and Mockito form a powerful combination for writing effective unit tests. This guide will cover all aspects of using these tools together with practical examples and best practices.

JUnit 5 Fundamentals

JUnit 5 is the latest version of the popular Java testing framework, consisting of three main components:

  1. JUnit Jupiter - The new programming model and extension model
  2. JUnit Vintage - Support for running JUnit 3 and 4 tests
  3. JUnit Platform - Foundation for launching testing frameworks

Basic Test Structure

 
import org.junit.jupiter.api.*;

class BasicTest {

    @BeforeAll
    static void setupAll() {
        System.out.println("Runs once before all tests");
    }

    @BeforeEach
    void setup() {
        System.out.println("Runs before each test");
    }

    @Test
    void testSomething() {
        Assertions.assertTrue(true);
    }

    @AfterEach
    void tearDown() {
        System.out.println("Runs after each test");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("Runs once after all tests");
    }
}
 

Key JUnit 5 Assertions

JUnit 5 provides a rich set of assertion methods:

 
@Test
void assertionsDemo() {
    // Basic assertions
    assertEquals(4, 2 + 2);
    assertNotEquals(3, 1 + 1);
    assertTrue(5 > 3);
    assertFalse(1 > 2);
    assertNull(null);
    assertNotNull("Hello");
    
    // Grouped assertions (all run before reporting failures)
    assertAll(
        () -> assertEquals(4, 2 * 2),
        () -> assertEquals("hello", "HELLO".toLowerCase())
    );
    
    // Exception testing
    Exception exception = assertThrows(
        ArithmeticException.class,
        () -> { int i = 1 / 0; }
    );
    assertEquals("/ by zero", exception.getMessage());
}
 

Mockito for Test Doubles

Mockito is the most popular mocking framework for Java, allowing you to create and configure test doubles (mocks, stubs, spies).

Mockito Core Concepts

  1. Mock - A complete dummy implementation that returns configured values
  2. Stub - A mock with predefined responses to method calls
  3. Spy - A partial mock that delegates to the real object by default
  4. Verification - Checking how mocks were interacted with

Basic Mockito Setup 

First, add Mockito to your project (Maven example):

 
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>
 

Creating and Using Stubs

A stub is a simplified or hard-coded implementation used to isolate and test parts of the system. It returns a fixed value to simulate interactions with external systems (e.g., an HTTP call or database query). This is useful when you don’t want the actual behavior of the system under test to interact with external resources.

A stub doesn’t care about how it’s used. It just returns a predefined value. There’s no verification of interactions.

A mock not only returns a predefined value but also verifies the interactions. For example, it checks whether a method was called with specific arguments or the correct number of times.

 
Http http = (String url) -> {
    return "{\"address\":{"
            + "\"house_number\":\"324\","
            + "\"road\":\"North Tejon Street\","
            + "\"city\":\"Colorado Springs\","
            + "\"state\":\"Colorado\","
            + "\"postcode\":\"80903\","
            + "\"country_code\":\"us\""
            + "}}";
};

Creating and Using Mocks

 
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.List;

class MockCreationTest {

    @Test
    void simpleMockDemo() {
        // Create mock
        List<String> mockedList = Mockito.mock(List.class);
        
        // Stub behavior
        Mockito.when(mockedList.get(0)).thenReturn("first");
        Mockito.when(mockedList.size()).thenReturn(100);
        
        // Use mock
        assertEquals("first", mockedList.get(0));
        assertEquals(100, mockedList.size());
        
        // Verify interactions
        Mockito.verify(mockedList).get(0);
        Mockito.verify(mockedList, Mockito.times(1)).size();
    }
}
 

Advanced Mockito Features

Argument Matchers

Mockito provides flexible argument matching:

 
@Test
void argumentMatchersDemo() {
    List<String> mockList = Mockito.mock(List.class);
    
    // Match any int argument
    Mockito.when(mockList.get(Mockito.anyInt())).thenReturn("element");
    
    // Match specific patterns
    Mockito.when(mockList.add(Mockito.argThat(arg -> arg.length() > 5)))
           .thenReturn(true);
    
    assertEquals("element", mockList.get(999));
    assertTrue(mockList.add("long string"));
    
    // Verify with matchers
    Mockito.verify(mockList).get(Mockito.anyInt());
}
 

Capturing Arguments

Mockito allows you to capture arguments passed to mock methods during interactions. This can be useful for verifying the arguments passed to a method.

 
@Test
void argumentCaptorDemo() {
    List<String> mockList = Mockito.mock(List.class);
    mockList.add("one");
    mockList.add("two");
    
    ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
    Mockito.verify(mockList, Mockito.times(2)).add(captor.capture());
    
    List<String> allValues = captor.getAllValues();
    assertEquals("one", allValues.get(0));
    assertEquals("two", allValues.get(1));
}
 

Mocking Static Methods

Since Mockito 3.4.0, you can mock static methods:

 
@Test
void staticMockDemo() {
    try (MockedStatic<Math> mockedMath = Mockito.mockStatic(Math.class)) {
        mockedMath.when(() -> Math.max(1, 2)).thenReturn(5);
        
        assertEquals(5, Math.max(1, 2));
        
        mockedMath.verify(() -> Math.max(1, 2));
    }
    
    // Original behavior restored after try block
    assertEquals(2, Math.max(1, 2));
}
 

Integration with JUnit 5

Mockito provides special integration with JUnit 5 through the @ExtendWith annotation.

Using MockitoExtension

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

@ExtendWith(MockitoExtension.class)
class MockitoJUnit5Test {
    // Test methods here
}
 

Annotation-based Mock Creation

@Mock: Creates a mock instance of the specified class or interface.

@InjectMocks: Automatically injects the mock dependencies into the class under test.

MockitoAnnotations.initMocks(this): Initializes the mocks annotated with @Mock and injects them into the class under test.

 
@ExtendWith(MockitoExtension.class)
class AnnotationBasedTest {
    
    @Mock
    List<String> mockList;
    
    @InjectMocks
    MyService myService;
    
    @Test
    void testWithInjectedMocks() {
        Mockito.when(mockList.size()).thenReturn(10);
        
        assertEquals(10, myService.processList());
    }
}

class MyService {
    private final List<String> list;
    
    public MyService(List<String> list) {
        this.list = list;
    }
    
    public int processList() {
        return list.size();
    }
}
 

When using Mockito with JUnit 5, proper initialization of mock objects is crucial for your

tests to work correctly. There are two primary ways to initialize mocks in JUnit 5:

  1. Manual initialization using MockitoAnnotations.openMocks(this)
  2. Automatic initialization using @ExtendWith(MockitoExtension.class)

MockitoAnnotations.openMocks(this) 

MockitoAnnotations.openMocks(this) is a method that:

  • Scans the test class instance for fields annotated with Mockito annotations (@Mock@Spy, etc.)
  • Initializes these fields with appropriate mock objects
  • Returns an AutoCloseable that can be used to release resources

When to Use It

You should use openMocks when:

  1. You’re not using the Mockito extension
  2. You need more control over the mock initialization process
  3. You’re working with legacy code or mixed JUnit 4/5 environments

Example Usage

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;

class ManualMockInitializationTest {
    
    @Mock
    List<String> mockList;
    
    private AutoCloseable closeable;
    
    @BeforeEach
    void setUp() {
        // Initialize mocks
        closeable = MockitoAnnotations.openMocks(this);
    }
    
    @Test
    void testMockBehavior() {
        // Configure mock
        when(mockList.size()).thenReturn(10);
        
        // Test behavior
        assertEquals(10, mockList.size());
    }
    
    @AfterEach
    void tearDown() throws Exception {
        // Release resources
        closeable.close();
    }
}

Important Notes

  1. Resource Management: The method returns an AutoCloseable that must be closed to avoid memory leaks
  1. JUnit 5 Integration: Typically called in @BeforeEach and closed in @AfterEach
  1. Alternative: In JUnit 4, you would use MockitoAnnotations.initMocks(this)

@ExtendWith(MockitoExtension.class) 

The MockitoExtension is a JUnit 5 extension that:

  • Automatically initializes mock objects
  • Handles all the mock lifecycle management
  • Provides cleaner test code by removing boilerplate

When to Use It

This is the preferred approach for:

  1. New JUnit 5 test classes
  2. When you want cleaner, more concise test code
  3. When using other JUnit 5 extensions

Example Usage

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

@ExtendWith(MockitoExtension.class)
class AutomaticMockInitializationTest {
    
    @Mock
    List<String> mockList;
    
    @Test
    void testMockBehavior() {
        // Configure mock
        when(mockList.size()).thenReturn(10);
        
        // Test behavior
        assertEquals(10, mockList.size());
    }
}

Key Differences

FeatureopenMocks(this)@ExtendWith
InitializationManualAutomatic
BoilerplateRequires setup/teardown methodsNo extra code needed
Resource ManagementManual via AutoCloseableHandled automatically
JUnit VersionWorks with both JUnit 4 and 5JUnit 5 only
IntegrationMore explicit controlMore declarative
Other ExtensionsCan be combined with others manuallyCleanly combines with other extensions


Stubbing vs. Mocking

Stubs (Simple Test Doubles)

 
@Test
void stubExample() {
    Http httpStub = Mockito.mock(Http.class);
    
    // Stub with fixed response
    Mockito.when(httpStub.get(Mockito.anyString()))
           .thenReturn("{\"status\":\"success\"}");
    
    // Use stub
    String response = httpStub.get("http://example.com");
    assertEquals("{\"status\":\"success\"}", response);
    
    // No verification of interactions
}
 

Mocks (Behavior Verification)

 
@Test
void mockExample() {
    Http httpMock = Mockito.mock(Http.class);
    
    // Stub behavior
    Mockito.when(httpMock.get("http://example.com"))
           .thenReturn("{\"status\":\"success\"}");
    
    // Use mock
    MyHttpClient client = new MyHttpClient(httpMock);
    client.fetchData();
    
    // Verify interactions
    Mockito.verify(httpMock).get("http://example.com");
    Mockito.verify(httpMock, Mockito.never()).post(Mockito.anyString());
}
 

Spies: Partial Mocks

 
@Test
void spyExample() {
    List<String> realList = new ArrayList<>();
    List<String> spyList = Mockito.spy(realList);
    
    // Real method called
    spyList.add("one");
    spyList.add("two");
    
    // Stub specific method
    Mockito.when(spyList.size()).thenReturn(100);
    
    assertEquals(2, realList.size()); // Real list has 2 elements
    assertEquals(100, spyList.size()); // Stubbed to return 100
    
    // Verify interactions
    Mockito.verify(spyList).add("one");
    Mockito.verify(spyList).add("two");
    Mockito.verify(spyList).size();
}
 

Mock : A full dummy implementation. No real methods are called unless explicitly configured.

Default behavior: All methods return default values (null for objects, 0 for integers, etc.) unless you specify stubbing.

Use case: When you want to isolate the behavior of a class without executing real code.

Spy : A partial mock that wraps an existing instance and allows you to control specific methods while keeping the real behavior for others.

Default behavior: Real methods are called unless explicitly stubbed.

Use case: When you want to mock specific methods but keep the real behavior for others.

Best Practices

  1. Use dependency injection for testability:
 
public class OrderService {
    private final PaymentGateway paymentGateway;
    
    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
    
    public boolean processOrder(Order order) {
        return paymentGateway.charge(order.getTotal());
    }
}
 
  1. Prefer constructor injection for mandatory dependencies:
 
@Test
void orderServiceTest() {
    PaymentGateway mockGateway = Mockito.mock(PaymentGateway.class);
    OrderService service = new OrderService(mockGateway);
    
    Order testOrder = new Order(100.0);
    Mockito.when(mockGateway.charge(100.0)).thenReturn(true);
    
    assertTrue(service.processOrder(testOrder));
}
 
  1. Avoid over-mocking - Only mock what you need to:
 
// Bad - mocking everything
@Test
void overMockingExample() {
    UserService mockService = Mockito.mock(UserService.class);
    UserDao mockDao = Mockito.mock(UserDao.class);
    EmailService mockEmail = Mockito.mock(EmailService.class);
    // ... and so on
    
    // This test becomes brittle and hard to maintain
}

// Better - mock only external dependencies
@Test
void betterMockingExample() {
    EmailService mockEmail = Mockito.mock(EmailService.class);
    UserRepository realRepo = new InMemoryUserRepository(); // Real implementation
    UserService service = new UserService(realRepo, mockEmail);
    
    // Test with real repository but mocked email
}
 
  1. Use meaningful verification:
 
@Test
void meaningfulVerification() {
    AuditService mockAudit = Mockito.mock(AuditService.class);
    OrderProcessor processor = new OrderProcessor(mockAudit);
    
    processor.process(new Order());
    
    // Good - verify the important interaction
    Mockito.verify(mockAudit).logOrder(Mockito.any(Order.class));
    
    // Avoid over-verification
    // Mockito.verifyNoMoreInteractions(mockAudit); // Often too strict
}
 

Handling Method Calls: when() vs doReturn()

When you use Mockito.when(), the real method is called first to evaluate the arguments. This can be problematic if the method has side effects (e.g., I/O operations). To avoid invoking the real method, you can use Mockito.doReturn().

Why doReturn() is safer: It directly configures the mock behavior without calling the real method.

Counter spyCounter = Mockito.spy(new Counter());

// This will invoke the real method first and then stub it, which could lead to unintended behavior.
Mockito.when(spyCounter.increment()).thenReturn(100);

// Use doReturn() to safely stub methods without invoking the real method.
Mockito.doReturn(100).when(spyCounter).increment();

Testing Exceptions

 
@Test
void exceptionTesting() {
    PaymentService mockService = Mockito.mock(PaymentService.class);
    
    // Stub to throw exception
    Mockito.when(mockService.process(Mockito.anyDouble()))
           .thenThrow(new PaymentFailedException("Insufficient funds"));
    
    OrderProcessor processor = new OrderProcessor(mockService);
    
    assertThrows(PaymentFailedException.class, () -> {
        processor.checkout(new Order(100.0));
    });
}
 

Advanced Mockito Techniques

Deep Stubs

 
@Test
void deepStubExample() {
    OrderService mockService = Mockito.mock(OrderService.class, Mockito.RETURNS_DEEP_STUBS);
    
    // Chain calls without NullPointerException
    Mockito.when(mockService.getOrder().getCustomer().getName()))
           .thenReturn("John Doe");
    
    assertEquals("John Doe", mockService.getOrder().getCustomer().getName());
}
 

Mocking Final Classes/Methods

Since Mockito 2.1.0, you can mock final classes and methods by creating a file: src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

With content: 

mock-maker-inline
 
@Test
void finalClassMocking() {
    FinalClass mock = Mockito.mock(FinalClass.class);
    
    Mockito.when(mock.finalMethod()).thenReturn("mocked");
    
    assertEquals("mocked", mock.finalMethod());
}
 

Integration Testing with JUnit 5 and Mockito

While unit tests focus on isolated components, integration tests verify how components work together.

 
@ExtendWith(MockitoExtension.class)
class OrderIntegrationTest {
    
    @Mock
    PaymentGateway paymentGateway;
    
    @Mock
    InventoryService inventoryService;
    
    @InjectMocks
    OrderProcessor orderProcessor;
    
    @Test
    void completeOrderIntegration() {
        Order order = new Order("123", 100.0, List.of("item1", "item2"));
        
        Mockito.when(inventoryService.checkStock("item1")).thenReturn(true);
        Mockito.when(inventoryService.checkStock("item2")).thenReturn(true);
        Mockito.when(paymentGateway.charge(100.0)).thenReturn(true);
        
        OrderResult result = orderProcessor.process(order);
        
        assertTrue(result.isSuccess());
        Mockito.verify(inventoryService).reserveItems(order.getItems());
        Mockito.verify(paymentGateway).charge(100.0);
    }
}
 

Parameterized Tests with JUnit 5

JUnit 5 supports parameterized tests for running the same test with different inputs.

 
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, 7})
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
    assertTrue(NumberUtils.isOdd(number));
}

@ParameterizedTest
@CsvSource({
    "2, 3, 5",
    "10, 20, 30",
    "0, 0, 0"
})
void addNumbers(int a, int b, int expected) {
    assertEquals(expected, Calculator.add(a, b));
}
 

Conclusion

JUnit 5 and Mockito form a powerful combination for testing Java applications. By understanding their features and best practices, you can write maintainable, reliable tests that verify your application’s behavior while remaining flexible to change. Remember:

  1. Use JUnit 5 for test structure and assertions
  2. Use Mockito for mocking dependencies
  3. Follow dependency injection principles for testability
  4. Write focused unit tests with clear verification
  5. Combine with integration tests for broader coverage

Post a Comment

Previous Post Next Post