While working on my Spring Boot app I asked ChatGPT for advice on how to deal with errors and exceptions related to the use of RestTemplate and Jackson’s ObjectMapper. ChatGPT offered to learn me unit testing on such code using Mockito. I was familiar with JUnit 5 and AssertJ but Mockito was new for me so I’m gonna dig into that rabbit hole.

The Mockito website is here.

Mockito, Java 21 and warning

Like JUnit 5, AssertJ and Hamcrest, Mockito is included as transitive dependency in spring-boot-starter-test. If you choose Java 21 for your project and do a test you will notice a warning related to Mockito, related to the loading of agents. Mockito loads an agent, more precisely the ByteBuddy agent, to enable the inline-mock-maker. This will not be allowed anymore in future Java versions, but as long as you don’t use the very recent versions the warning can be ignored. With help from ChatGPT I tried to get rid of it, using the Maven Surefire plugin, but that wasn’t worth the trouble.

How it works

Mockito isolates methods by allowing you to create mocks of dependencies. Internally Mockito uses reflection for it. When doing integration testing with @SpringBootTest the beans of your application are all being created and wired so you work in a real application context. With Mockito unit testing the beans and other object instances you need are not the real but fake ones. You can tell these mock objects how to behave when certain specific methods are called on them. This is enough to be able to see if your method, for example, really returns the type of value or exception that you expect it to return.

Imports

You need a few imports to begin with:

import org.mockito.Mockito;
#or
import static org.mockito.Mockito.*;

and

import static org.mockito.ArgumentMatchers;
#or
import static org.mockito.ArgumentMatchers.*;

You can choose the static variants for convenience, it saves you some code. The Mockito website shows its examples using the static variants.

Creating a mock object

To create a mock object you use the mock method with an argument of type class. Depending on the manner in which you did the imports, you get one of these two:

TownRepository repo = mock(TownRepository.class);

TownRepository repo = Mockito.mock(TownRepository.class);

You can create mock objects as well with the @Mock annotation but this seems to require some additional @BeforeEach initialization.

Stubbing with .thenReturn() and .thenAnswer()

A stub is a line in which you tell the mock object what it should return if a specific method is called on it with a specific argument.

when(repo.findById(1L)).thenReturn(amsterdam);
when(repo.findById(2L)).thenReturn(rotterdam);

The most common pattern is when…thenReturn but it is also possible to use when…thenAnswer. The latter lets you use a lambda expression as argument. ChatGPT gave this example:

void stubWithMap() {
    WoonplaatsRepository repo = mock(WoonplaatsRepository.class);

    Map<Long, Woonplaats> db = new HashMap<>();
    db.put(1L, new Woonplaats("AMS", "Amsterdam"));
    db.put(2L, new Woonplaats("RTD", "Rotterdam"));

    when(repo.findById(anyLong()))
        .thenAnswer(invocation -> db.get(invocation.getArgument(0)));

    System.out.println(repo.findById(1L)); // AMS - Amsterdam
    System.out.println(repo.findById(2L)); // RTD - Rotterdam
    System.out.println(repo.findById(3L)); // null
}

The invocation parameter in the lambda expression refers to the ‘findById(anyLong())’ method (more about ‘anyLong’ later). invocation.getArgument(0) gives you the first argument, but you can use as well invocation.getMethod().getName() to get the method name for example. It is even possible to use invocation.getMock to get the mock instance itself.

This code snippet lets you kind of mock a database in the form of a map that has domain objects as values. Given that with JPA database rows are converted to domain objects and vice versa, this might be quite useful.

Argument matchers

In a when…thenReturn statement you can give specific arguments or you may generalize them to their class or type. In the latter case you need the ArgumentMatchers import. This class has a long list of methods, mostly starting with ‘any’, that you use to provide an argument that can, for example, be of Integer or String type.

The javadoc page provides explanation and examples.

Mockito’s javadoc page warns that if you use some ‘any(Class)’ method in a when…thenReturn statement, calling the method with a null statement will not pass as correct. This is the specific example they provide:

 // stubbing using anyBoolean() argument matcher
 when(mock.dryRun(anyBoolean())).thenReturn("state");

 // below the stub won't match, and won't return "state"
 mock.dryRun(null);

 // either change the stub
 when(mock.dryRun(isNull())).thenReturn("state");
 mock.dryRun(null); // ok

 // or fix the code ;)
 when(mock.dryRun(anyBoolean())).thenReturn("state");
 mock.dryRun(true); // ok

Argument matchers - useful methods

There is a long list of methods, see here. Any basic Java type has one, like:

  • anyInt()
  • anyBoolean()
  • anyString()

Collections are also represented:

  • anyCollection()
  • anyList()
  • anyIterable()

If you have a custom object (for example some domain or DTA object:

  • any(Class<T> type)

If it can be anything:

  • any()

Three String-related matchers that might be helpful:

  • endsWith(String suffix)
  • startsWith(String prefix)
  • contains(String substring)

Dealing with null:

  • nullable(Class<T> clazz) explanation: argument can be either null or of specified class type
  • isNull
  • isNotNull, notNull

Using literal argument with a matcher:

  • eq()

Argument matchers - eq

The Mockito documentation states the following:

“If you are using argument matchers, all arguments have to be provided by matchers.”

When a method with multiple arguments is stubbed, you might want ‘any’ value for one argument and a very specific value for another argument. You cannot simply mix these two categories in one argument list. Instead, you need to use ArgumentMatchers.eq method on the spicific method. The following example illustrates it:

 verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
 //above is correct - eq() is also an argument matcher

 verify(mock).someMethod(anyInt(), anyString(), "third argument");
 //above is incorrect - exception will be thrown because third argument is given without argument matcher.

Verify

Verify is used to check if some method has been called on a mock object. Mockito remembers the history of method invocations on mock objects and with verify you can make sure that a certain invocation took place. Example:

LinkedList mockedList = mock(LinkedList.class);

//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

mockedList.get(0)

verify(mockedList).get(0);

The verify method returns the mock object itself, which allows for nice chaining of methods. If the verify test fails, an error is thrown, which means the test will fail. This differs from the normal method invocations that you do. Here the test will not fail. See the error throwing paragraph below.

Error throwing

It is important to note that Mockito, unlike AssertJ, does not automatically throw errors that result in failed tests. It does so for verify but not for regular method invocations. Instead, when no stub is available or no stub matches, it returns either null, 0 or an empty collection. These are the so-called default return values. To make Mockito throw errors you can either change the when…thenReturn into an when….thenThrow(someException), or you can turn on strict mode with these two annotations on your test class:

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
class MyTest { ... }

According to ChatGPT this is the way professionals mostly do it. The when…thenThrow way requires more code but is useful when you want very strict and specific testing of some method.


<
Previous Post
AssertJ
>
Next Post
SSH and Linux