Mockito @Captor Annotation

November 17, 2020 No comments Mockito Java Framework Testing Captor

1. Introduction

Mockito is a Java-based framework for mocking objects in unit tests. The library allows complex verifications of method calls and input parameters. In this article, we are going to present the @Captor annotation used to capturing method argument values for future assertions.

2. Capturing the arguments

@Captor annotation, which always occurs in conjunction with ArgumentCaptor, allows defining special objects used to capturing the arguments passed to a method that we want to inspect. Capturing input parameters is extremely useful for testing methods called in other methods. Sometimes this is the only way to test logic that stands behind some huge implementation that doesn't return any "on the way".

Let's start with a simple example:

package com.frontbackend.libraries.mockito;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class MockitoCaptorTest {

    @Mock
    private List<String> list;

    @Captor
    private ArgumentCaptor<String> valueCaptor;

    @Test
    public void shouldCaptureListParameters() {
        list.add("one");
        list.add("two");

        verify(list, times(2)).add(valueCaptor.capture());

        List<String> allValues = valueCaptor.getAllValues();

        Assert.assertTrue(allValues.contains("one"));
        Assert.assertTrue(allValues.contains("two"));

        Assert.assertEquals("two", valueCaptor.getValue());
    }
}

In this sample JUnit test, we are using @Mock annotation to create a mock object of the List type. Then we used @Captor annotation to define the new ArgumentCaptor field of type String to store our captured arguments.

First, we add two String values to our list: "one", "two". Secondly, we are using the verify(...) method with the ArgumentCaptor to capture that strings.

Every ArgumentCaptor has two methods getValue() and getAllValues(). The getValue() can be used when we have captured an argument from a single method call. If the method was called multiple times getValue() will return the latest captured value.

The getAllValues returns the list of arguments if the method was called more than once.

3. Using @Captor in test

Previously we presented a simple JUnit test, now let's check an example from a more complex application:

Let's consider we have three services:

for summing:

package com.frontbackend.libraries.mockito.service;

public class SimpleSummingService {
    public double sum(double a, double b) {
        return a + b;
    }
}

for multiplication

package com.frontbackend.libraries.mockito.service;

public class SimpleMultiplicationService {
    public double multiplication(double a, double b) {
        return a * b;
    }
}

for sending emails:

package com.frontbackend.libraries.mockito.service;

public class SimpleMailService {
    public boolean sendEmail(String body) {
        // send email
        return true;
    }
}

We have also a complex service that uses these three services. It has a method businessLogic which is processing some data and calling other services. Note that this method doesn't return any value:

package com.frontbackend.libraries.mockito.service;

public class ComplexBusinessLogicService {

    private final SimpleSummingService simpleSummingService;
    private final SimpleMultiplicationService simpleMultiplicationService;
    private final SimpleMailService simpleMailService;

    private ComplexBusinessLogicService(SimpleSummingService simpleSummingService, SimpleMultiplicationService simpleMultiplicationService,
            SimpleMailService simpleMailService) {
        this.simpleSummingService = simpleSummingService;
        this.simpleMultiplicationService = simpleMultiplicationService;
        this.simpleMailService = simpleMailService;
    }

    public void businessLogic(double a, double b) {
        // some initial business logic
        double newA = a * 1000 + 90;
        double newB = b * 800 + 100;

        double sum = simpleSummingService.sum(newA, newB);

        // some more business logic
        sum = sum + 80;
        double multiplication = simpleMultiplicationService.multiplication(sum, 10);

        simpleMailService.sendEmail(String.format("This is calculated value: %s", multiplication));
    }
}

We want to write a test that will verify if calculations after calling each service are correct:

package com.frontbackend.libraries.mockito.service;

import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class ComplexBusinessLogicServiceTest {

    @Mock
    private SimpleMailService simpleMailService;

    @Mock
    private SimpleMultiplicationService simpleMultiplicationService;

    @Mock
    private SimpleSummingService simpleSummingService;

    @Captor
    private ArgumentCaptor<String> emailBodyArgCaptor;

    @Captor
    private ArgumentCaptor<Double> sumACaptor;

    @Captor
    private ArgumentCaptor<Double> sumBCaptor;

    @Captor
    private ArgumentCaptor<Double> mulACaptor;

    @Captor
    private ArgumentCaptor<Double> mulBCaptor;

    @InjectMocks
    private ComplexBusinessLogicService complexBusinessLogicService;

    @Test
    public void testComplexBusinessLogic() {
        // Given
        double a = 10;
        double b = 20;

        when(simpleMailService.sendEmail(anyString())).thenCallRealMethod();
        when(simpleMultiplicationService.multiplication(anyDouble(), anyDouble())).thenCallRealMethod();
        when(simpleSummingService.sum(anyDouble(), anyDouble())).thenCallRealMethod();

        // When
        complexBusinessLogicService.businessLogic(a, b);

        // Then
        verify(simpleSummingService).sum(sumACaptor.capture(), sumBCaptor.capture());
        Assert.assertEquals(Double.valueOf(a * 1000 + 90), sumACaptor.getValue());
        Assert.assertEquals(Double.valueOf(b * 800 + 100), sumBCaptor.getValue());

        verify(simpleMultiplicationService).multiplication(mulACaptor.capture(), mulBCaptor.capture());
        Assert.assertEquals(Double.valueOf(((a * 1000 + 90) + (b * 800 + 100) + 80)), mulACaptor.getValue());
        Assert.assertEquals(Double.valueOf(10), mulBCaptor.getValue());

        verify(simpleMailService).sendEmail(emailBodyArgCaptor.capture());
        Assert.assertEquals(String.format("This is calculated value: %s", ((a * 1000 + 90) + (b * 800 + 100) + 80) * 10),
                emailBodyArgCaptor.getValue());
    }
}

SimpleMailService, SimpleMultiplicationService and SimpleSummingService services are annotated with @Mock annotation but in test we want to call the actual implementation that why we used: .thenCallRealMethod() method. Mocked objects are injected into the class that we want to test - ComplexBusinessLogicService.

To test what happens inside a complex method we used several ArgumentCaptor objects for Double and String types. The rest of the tests are verifications if calculated parameters are equal to expected values.

4. Conclusion

In this article, we looked at how to use @Captor with an ArgumentCaptor object. We presented simple examples, and more complex than could be found in real-life applications with complicated business logic.

As usual, code snippets used in this tutorial are available over on GitHub.

{{ message }}

{{ 'Comments are closed.' | trans }}