Mockito @ExtendWith

Introduction

The @ExtendWith annotation in JUnit 5 is used to register extensions. Mockito provides a MockitoExtension class that can be used with the @ExtendWith annotation to enable Mockito annotations and simplify the creation of mocks. This tutorial will demonstrate how to use the @ExtendWith annotation with MockitoExtension to mock dependencies in a PaymentService class.

Maven Dependencies

To use Mockito with JUnit 5, add the following dependencies to your pom.xml file:

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

Example Scenario

We will create a PaymentService class that has a dependency on a PaymentRepository and a NotificationService. Our goal is to test the PaymentService methods using Mockito's @ExtendWith annotation to mock the dependencies.

PaymentService, PaymentRepository, and NotificationService Classes

First, create the Payment, PaymentRepository, and NotificationService classes.

public class Payment {
    private String transactionId;
    private double amount;

    // Constructor, getters, and setters
    public Payment(String transactionId, double amount) {
        this.transactionId = transactionId;
        this.amount = amount;
    }

    public String getTransactionId() {
        return transactionId;
    }

    public void setTransactionId(String transactionId) {
        this.transactionId = transactionId;
    }

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }
}

public interface PaymentRepository {
    void savePayment(Payment payment);
    Payment findPaymentByTransactionId(String transactionId);
}

public class NotificationService {
    public void notifyCustomer(String message) {
        // Code to notify customer
    }
}

public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final NotificationService notificationService;

    public PaymentService(PaymentRepository paymentRepository, NotificationService notificationService) {
        this.paymentRepository = paymentRepository;
        this.notificationService = notificationService;
    }

    public void processPayment(Payment payment) {
        paymentRepository.savePayment(payment);
        notificationService.notifyCustomer("Payment processed: " + payment.getTransactionId());
    }

    public Payment getPayment(String transactionId) {
        return paymentRepository.findPaymentByTransactionId(transactionId);
    }
}

JUnit 5 Test Class with Mockito

Create a test class for PaymentService using JUnit 5 and Mockito.

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class)
public class PaymentServiceTest {

    @Mock
    private PaymentRepository paymentRepository;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private PaymentService paymentService;

    @Captor
    private ArgumentCaptor<Payment> paymentCaptor;

    @Test
    public void testProcessPayment() {
        // Given
        Payment payment = new Payment("12345", 100.0);

        // When
        paymentService.processPayment(payment);

        // Then
        verify(paymentRepository).savePayment(paymentCaptor.capture());
        Payment capturedPayment = paymentCaptor.getValue();
        assertEquals("12345", capturedPayment.getTransactionId());
        assertEquals(100.0, capturedPayment.getAmount());
        verify(notificationService).notifyCustomer("Payment processed: 12345");
    }

    @Test
    public void testGetPayment() {
        // Given
        Payment mockPayment = new Payment("12345", 100.0);
        when(paymentRepository.findPaymentByTransactionId("12345")).thenReturn(mockPayment);

        // When
        Payment result = paymentService.getPayment("12345");

        // Then
        assertNotNull(result);
        assertEquals("12345", result.getTransactionId());
        assertEquals(100.0, result.getAmount());
        verify(paymentRepository).findPaymentByTransactionId("12345");
    }
}

Explanation of Annotations

  1. @ExtendWith(MockitoExtension.class):

    • This annotation integrates Mockito with JUnit 5. It tells JUnit to enable Mockito-specific features and process annotations such as @Mock, @InjectMocks, and @Captor.
    • By using MockitoExtension, you don't need to initialize the mocks manually. The extension takes care of creating and injecting mock instances.
  2. @Mock:

    • This annotation creates a mock instance of the specified class or interface. In this example, paymentRepository and notificationService are mocked.
    • Mocks are useful for simulating dependencies in isolation. They allow you to define behavior and verify interactions without needing real implementations.
  3. @InjectMocks:

    • This annotation injects the mocks marked with @Mock into the class under test. In this case, paymentRepository and notificationService are injected into PaymentService.
    • This is particularly useful for classes that have dependencies, as it ensures the class under test receives mock instances instead of real dependencies.
  4. @Captor:

    • This annotation creates an ArgumentCaptor for capturing arguments passed to mock methods. In this example, paymentCaptor captures the Payment object passed to savePayment.
    • Argument captors are useful for verifying the values of arguments passed to mocked methods, providing more insight into the interactions between objects.

Test Methods

  1. testProcessPayment:

    • Given: A new Payment object is created with a transaction ID and amount.
    • When: The processPayment method of paymentService is called with the payment object.
    • Then:
      • Verifies that the savePayment method was called on paymentRepository with a Payment object.
      • Captures the Payment object passed to savePayment using paymentCaptor.
      • Asserts that the captured payment's transaction ID and amount are correct.
      • Verifies that the notifyCustomer method was called on notificationService with the expected message.
  2. testGetPayment:

    • Given: A mock Payment object is created and configured to be returned by paymentRepository when the findPaymentByTransactionId method is called with a specific transaction ID.
    • When: The getPayment method of paymentService is called with the transaction ID.
    • Then:
      • Asserts that the returned payment is not null and that its transaction ID and amount are correct.
      • Verifies that the findPaymentByTransactionId method was called on paymentRepository with the expected transaction ID.

Conclusion

The @ExtendWith annotation in JUnit 5, combined with MockitoExtension, simplifies the creation and injection of mock objects in unit tests. By using @Mock, @InjectMocks, and @Captor, you can easily set up mock dependencies and focus on testing the behavior of your code. This step-by-step guide demonstrated how to effectively use the @ExtendWith annotation with MockitoExtension in your unit tests, covering different scenarios to ensure comprehensive testing of the PaymentService class.

Related Mockito Annotations

Comments