Mockito’s @Mock and @InjectMocks: A Deep Dive

Introduction

In unit testing, isolating the code under test is crucial for ensuring reliability and pinpointing the source of bugs. Mockito is a popular Java mocking framework that simplifies this process. Two of its core annotations, @Mock and @InjectMocks, work together to facilitate dependency injection and create controlled test environments. This tutorial will explain each annotation, demonstrate their usage, and highlight how they collaborate to build effective unit tests.

Understanding @Mock

The @Mock annotation instructs Mockito to create a mock object of the specified class. A mock object is a simulated version of a real dependency, allowing you to control its behavior and verify interactions without relying on the actual implementation.

Here’s a simple example:

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

interface DataService {
    String getData();
}

class MyComponent {
    private final DataService dataService;

    public MyComponent(DataService dataService) {
        this.dataService = dataService;
    }

    public String processData() {
        return "Processed: " + dataService.getData();
    }
}

public class MockitoExample {

    @Mock
    private DataService dataService;

    @Test
    public void testProcessData() {
        // Configure the mock to return a specific value
        Mockito.when(dataService.getData()).thenReturn("Test Data");

        MyComponent component = new MyComponent(dataService);
        String result = component.processData();

        assertEquals("Processed: Test Data", result);
    }
}

In this example, @Mock creates a mock DataService. We then define the mock’s behavior using Mockito.when() to return "Test Data" when getData() is called. This allows us to test MyComponent without needing a real DataService implementation.

Understanding @InjectMocks

The @InjectMocks annotation is used to create an instance of a class and automatically inject any mocked dependencies (created with @Mock) into it. This simplifies the setup process, as you don’t have to manually create and inject dependencies.

Consider this scenario:

import org.mockito.Mockito;
import org.mockito.annotations.InjectMocks;
import org.mockito.annotations.Mock;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

interface PaymentGateway {
    boolean processPayment(double amount);
}

class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public boolean placeOrder(double amount) {
        return paymentGateway.processPayment(amount);
    }
}

public class InjectMocksExample {

    @Mock
    private PaymentGateway paymentGateway;

    @InjectMocks
    private OrderService orderService;

    @Test
    public void testPlaceOrder() {
        // Configure the mock to return true for successful payment
        Mockito.when(paymentGateway.processPayment(100.0)).thenReturn(true);

        boolean result = orderService.placeOrder(100.0);

        assertTrue(result);
    }
}

Here, @InjectMocks creates an instance of OrderService and automatically injects the mocked paymentGateway into it. We don’t need to explicitly create an OrderService object and pass in the paymentGateway dependency. Mockito handles this injection for us.

Working with @Mock and @InjectMocks Together

The true power of these annotations is revealed when used together. @InjectMocks relies on @Mock to provide the mock dependencies it needs to inject.

Here’s a consolidated example:

import org.mockito.Mockito;
import org.mockito.annotations.InjectMocks;
import org.mockito.annotations.Mock;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

interface EmailService {
    void sendEmail(String recipient, String message);
}

class NotificationService {
    private final EmailService emailService;

    public NotificationService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void notifyUser(String user, String message) {
        emailService.sendEmail(user, message);
    }
}

public class CombinedExample {

    @Mock
    private EmailService emailService;

    @InjectMocks
    private NotificationService notificationService;

    @Test
    public void testNotifyUser() {
        // Configure the mock to do nothing when sendEmail is called
        Mockito.doNothing().when(emailService).sendEmail("[email protected]", "Test message");

        notificationService.notifyUser("[email protected]", "Test message");

        // Verify that sendEmail was called with the correct arguments
        Mockito.verify(emailService).sendEmail("[email protected]", "Test message");
    }
}

In this example, @Mock creates a mock EmailService, and @InjectMocks creates a NotificationService instance, injecting the mock EmailService into it. The test verifies that the sendEmail method on the mock EmailService was called with the expected arguments.

Initialization of Mocks

To ensure that the mocks are initialized correctly, you need to use either @RunWith(MockitoJUnitRunner.class) (JUnit 4) or @ExtendWith(MockitoExtension.class) (JUnit 5). Alternatively, you can call MockitoAnnotations.initMocks(this) in a @BeforeEach or @BeforeAll method.

JUnit 4 Example:

import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.annotations.InjectMocks;
import org.mockito.annotations.Mock;
import org.mockito.runners.MockitoJUnitRunner;

// ... (rest of the class remains the same)

@RunWith(MockitoJUnitRunner.class)
public class MyTest {
    // ...
}

JUnit 5 Example:

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

public class MyTest {
    @Mock
    private DataService dataService;

    @InjectMocks
    private MyComponent myComponent;

    @BeforeEach
    public void setup() {
        // Not needed when using @ExtendWith(MockitoExtension.class)
        // MockitoAnnotations.initMocks(this);
    }
    //...
}

Conclusion

@Mock and @InjectMocks are essential annotations for writing effective unit tests with Mockito. They simplify dependency injection, promote testability, and enable you to isolate your code for reliable and focused testing. By understanding how these annotations work together, you can create more maintainable and robust unit tests that contribute to the overall quality of your software.

Leave a Reply

Your email address will not be published. Required fields are marked *