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.