Learn Java

Java Mockito

Unit tests are well-known for their key features: they are small, fast, and designed to test a unit of code in isolation. However, sometimes it is not easy to comply with these criteria, especially if your code unit under test depends on external resources, such as files, databases, or web services. There are several tools that help you isolate a unit of code from its dependencies, and one of them is the use of so-called mock objects.

A mock object is a simulated object that imitates the behavior of a real object in controlled ways. In unit tests, you can use mock objects instead of real objects to interact with the code under test and verify the result of such interaction. You can create mock objects manually or use a mocking framework that will do the heavy lifting for you.

Mockito

One of the most popular mocking frameworks for Java is the Mockito framework. It has a simple and convenient API, allows you to write clean and concise tests, and can be used together with JUnit.

First, let's see how to add Mockito to your project.

If you have a Maven project, add the following dependencies:

<dependencies>
    <dependency>
<!--        JUnit5 -->
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.7.1</version>
        <scope>test</scope>
    </dependency>

    <dependency>
<!--        Mockito -->
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>3.11.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

For a Gradle project, add the following:

dependencies {
// JUnit5
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
// Mockito
    testImplementation 'org.mockito:mockito-core:3.11.2'
}

Mockito developers regularly release new versions, so always check for the most recent version available

If your class has a dependency...

Now, let's consider a situation where you would need to use Mockito for your unit tests.

In our example, we have a class named FXConverter responsible for converting an amount in one currency to the proportionate amount in another currency. For this purpose, it uses the convert method that accepts the source and the target currency codes, together with the input amount, as String and returns the converted amount as BigDecimalFXConverter has a RemoteFXRateService dependency, which has the getRate method that sends a request to a foreign exchange web service to retrieve the actual exchange rate between the source currency and the target currency and return it as String. This method can throw an IllegalArgumentException if it receives a negative response from the web service for the given pair of currency codes passed as arguments. It can also throw an IllegalStateException if the web service does not respond for any reason. In case of an error, convert returns the BigDecimal equivalent of "-1.00", and in the case of success, it returns the converted amount. It's that simple.

Here is the code of the FXConverter class:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class FXConverter {
    private final RemoteFXRateService remoteFXRateService;

    public FXConverter(RemoteFXRateService remoteFXRateService) {
        this.remoteFXRateService = remoteFXRateService;
    }

    public BigDecimal convert(String source, String target, String input) {
        try {
            String response = remoteFXRateService.getRate(source, target);
            BigDecimal rate = new BigDecimal(response);
            BigDecimal amount = new BigDecimal(input);

            return amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
        } catch (IllegalStateException | IllegalArgumentException ex) {
            return new BigDecimal("-1.00");
        }
    }
}

Now that it's done, we want to write unit tests to make sure that our FXConverter works as intended. As you can see, we cannot simply create an instance of the FXConverter class because it has the RemoteFXRateService class as a dependency, so we have to instantiate that class first. Here we encounter some potential problems.

First, RemoteFXRateService can also have some dependencies, and those dependencies can have dependencies too… In other words, the number of objects we have to instantiate can grow like a snowball.

Second, we want to test our FXConverter class in isolation with all its dependencies tested beforehand. Also, we want to test our class in controlled conditions and avoid hard reliance on external data.

Fortunately, Mockito allows us to avoid these difficulties and provides an easy-to-use API to create mock objects and define their behavior.

Creating mock objects

Let's set up unit tests for our FXConverter class. There are two ways to instantiate a mock object of the RemoteFXRateService class. The first one is with the use of mock, a static method that creates a default mock of the specified class.

import static org.mockito.Mockito.mock;

class FXConverterTest {

    private RemoteFXRateService service = mock(RemoteFXRateService.class);

    private FXConverter converter = new FXConverter(service);

}

The second way is using annotations. If you want to use this method, you have to add another dependency to your project:

testImplementation 'org.mockito:mockito-junit-jupiter:3.11.2'

Then you can set up the required classes using the appropriate annotations:

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

@ExtendWith(MockitoExtension.class)
class FXConverterTest {

    @Mock
    private RemoteFXRateService service;

    private FXConverter converter;

    @BeforeEach
    void setup() {
        converter = new FXConverter(service);
    }
}

If you are unsure which of these two methods to choose, keep in mind that while the mock method requires less code, it only supports raw classes, so you cannot use it to instantiate, for example, List<String>; it can only generate a raw List.

Defining the behavior

Next, we need to define the behavior of the mock object for different test cases. We want to test the convert method using a number of edge cases, including the situations in which the getRate method throws an IllegalArgumentException or anIllegalStateException, and when it returns the exchange rate.

For this purpose, Mockito has other static methods: when and thenReturn. They define the answer that must be returned when the specified method of the mock object is invoked with the specified arguments. Look at the following example:

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class FXConverterTest {

    private RemoteFXRateService service = mock(RemoteFXRateService.class);

    private FXConverter converter = new FXConverter(service);

    @Test
    @DisplayName("Given 100.00 USD, when convert to USD, then return 100.00")
    void test1() {
        when(service.getRate("USD", "USD")).thenReturn("1.0000");

        BigDecimal result = converter.convert("USD", "USD", "100.00");

        assertEquals("100.00", result.toString());
    }
}

Here we specify that if the getRate method of the service object is called with the same two arguments, "USD" and "USD", then the method must return "1.0000", because the exchange rate of USD to USD, in any case, will be equal to 1.0. Then we call the convert method and assert that the returned value is effectively equal to the value passed as an argument.

Now if we run this test, we will see that it is successfully passed:

FXConverterTest > Given 100.00 USD, when convert to USD, then return 100.00 PASSED

Let's run it for two different currencies:

@Test
@DisplayName("Given 100.00 USD, when convert to EUR, then return 84.97")
void test2() {
    when(service.getRate("USD", "EUR")).thenReturn("0.8497");

    BigDecimal result = converter.convert("USD", "EUR", "100.00");

    assertEquals("84.97", result.toString());
}

Again, the test is passed successfully. But what if we call the convert method with a different pair of arguments, for example, "USD" and "GBP"? The test will fail because we did not define any answer for this combination of arguments for getRate.

Argument matchers

Mockito provides default behavior for all created mock objects, so any unspecified method calls will return the following values:

  • null for objects
  • 0 for numbers
  • false for boolean
  • empty collections for collections

Mockito has the ArgumentMatchers class whose static methods give us great flexibility in specifying arguments: we can define the behavior of a method depending on whether the argument is equal or not equal to a certain value, is of a certain type, is null or not null, matches to a certain pattern, etc. You can find the complete list of the argument matchers in the official documentation.

Here are some examples based on our case:

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.when;

class FXConverterTest {

    private RemoteFXRateService service = mock(RemoteFXRateService.class);

    private FXConverter converter = new FXConverter(service);

    @Test
    void test3() {
        // 1st arg is "USD" and 2nd arg is any string that contains "coin"
        when(service.getRate(eq("USD"), contains("coin")))
                .thenReturn("0.0000");

        // both 1st arg and 2nd arg is any string
        when(service.getRate(anyString(), anyString())).thenReturn("42");

        // 1st arg is any string that starts with "US" 
        // and 2nd arg is any string that ends with "BP"
        when(service.getRate(startsWith("US"), endsWith("BP")))
                .thenReturn("0.7266");
    }

}

Common argument matchers are:

If you are using argument matchers, all arguments have to be provided by matchers:

when(mock.someMethod(anyInt(), eq("second argument"))).thenReturn(42);// correct because eq() is also an argument matcher

when(mock.someMethod(anyInt(), "second argument")).thenReturn(42);// incorrect because "second argument" is not an argument matcher.

Throwing exceptions

Finally, let's see how to change the behavior of any mocked method to make it throw an exception. For this purpose, Mockito has another static method called thenThrow (do not forget to import it):

@Test
@DisplayName("Given any args, when service throws exception, then return -1.00")
void test4() {
    when(service.getRate(anyString(), anyString()))
            .thenThrow(new IllegalStateException());

    BigDecimal result =
            converter.convert("USD", "EUR", "100.00");

    assertEquals("-1.00", result.toString());
}

Whenever the getRate method throws an IllegalStateException, the convert method returns the BigDecimal equivalent of "-1.00". If we run this test, we will see that it is successfully passed:

FXConverterTest> Given any args, when service throws exception, then return -1.00 PASSED

The test for the situation when the getRate throws an IllegalArgumentException can be set up in the same way.

Conclusion

It is important to know how to add Mockito to your project and then use it together with JUnit to write unit tests. To test a unit of code in isolation from its dependencies, we may create mock objects. For the class under test and the testing framework, mock objects behave exactly the same way as the actual objects and have the required functionality defined by the programmer. Mockito provides us with great flexibility for defining the desired behavior of mock objects with the help of the ArgumentMatchers class.

Mocking is a useful technique but it is not always necessary. Reserve it for situations when your test might actually benefit from its use.

Create a free account to access the full topic

“It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.”
Andrei Maftei
Hyperskill Graduate