Demystifying Static Mocking With Mockito
Many times, we have to deal with a situation when our code invokes a static method. Discover a possible solution for mocking static methods with Mockito.
Join the DZone community and get the full member experience.
Join For FreeThese days, writing tests is a standard part of development. Unfortunately, we need to deal from time to time with a situation when a static method is invoked by a tested component. Our goal is to mitigate this part and avoid third-party component behavior. This article sheds light on the mocking of static methods by using the "inline mock maker" introduced by Mockito in the 3.4 version. In other words, this article explains Mockito.mockStatic
method in order to help us with unwanted invocation of the static method.
In This Article, You Will Learn
- How to mock and verify static methods with
mockStatic
feature - How to setup
mockStatic
in different Mockito versions
Introduction
Many times, we have to deal with a situation when our code invokes a static method. It can be our own code (e.g., some utility class or class from a third-party library). The main concern in unit testing is to focus on the tested component and ignore the behavior of any other component (including static methods). An example is when a tested method in component A is calling an unrelated static method from component B.
Even so, it's not recommended to use static methods; we see them a lot (e.g., utility classes). The reasoning for avoiding the usage of static methods is summarized very well in Mocking Static Methods With Mockito.
Generally speaking, some might say that when writing clean object-orientated code, we shouldn’t need to mock static classes. This could typically hint at a design issue or code smell in our application.
Why? First, a class depending on a static method has tight coupling, and second, it nearly always leads to code that is difficult to test. Ideally, a class should not be responsible for obtaining its dependencies, and if possible, they should be externally injected.
So, it’s always worth investigating if we can refactor our code to make it more testable. Of course, this is not always possible, and sometimes we need to mock static methods.
A Simple Utility Class
Let's define a simple SequenceGenerator
utility class used in this article as a target for our tests. This class has two "dumb" static methods (there's nothing fancy about them). The first nextId
method (lines 10-12) generates a new ID with each invocation, and the second nextMultipleIds
method (lines 14-20) generates multiple IDs as requested by the passed argument.
@UtilityClass
public class SequenceGenerator {
private static AtomicInteger counter;
static {
counter = new AtomicInteger(1);
}
public static int nextId() {
return counter.getAndIncrement();
}
public static List<Integer> nextMultipleIds(int count) {
var newValues = new ArrayList<Integer>(count);
for (int i = 0; i < count; i++) {
newValues.add(counter.getAndIncrement());
}
return newValues;
}
}
MockedStatic
Object
In order to be able to mock static methods, we need to wrap the impacted class by "inline mock maker." The mocking of static methods from our SequenceGenerator
class introduced above is achievable by MockedStatic
instance retrieved via Mockito.mockStatic
method. This can be done as:
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
...
}
Or
MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class));
...
seqGeneratorMock.close();
The created mockStatic
instance has to be always closed. Otherwise, we risk ugly side effects in next tests running in the same thread when the same static method is involved (i.e., SequenceGenerator
in our case). Therefore, the first option seems better, and it is used in most articles on this topic. The explanation can be found on the JavaDoc site (chapter 48) as:
When using the inline mock maker, it is possible to mock static method invocations within the current thread and a user-defined scope. This way, Mockito assures that concurrently and sequentially running tests do not interfere. To make sure a static mock remains temporary, it is recommended to define the scope within a try-with-resources construct.
To learn more about this topic, check out these useful links:
Mock Method Invocation
Static methods (e.g., our nextId
or nextMultipleIds
methods defined above) can be mocked with MockedStatic.when
. This method accepts a functional interface defined by MockedStatic.Verification
. There are two cases we can deal with.
Mocked Method With No Argument
The simplest case is mocking a static method with no argument (nextId
method in our case). In this case, it's sufficient to pass to seqGeneratorMock.when
method only a method reference (see line 5). The returned value is specified in a standard way (e.g., with thenReturn
method).
@Test
void whenWithoutArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
int newValue = 5;
seqGeneratorMock.when(SequenceGenerator::nextId).thenReturn(newValue);
assertThat(SequenceGenerator.nextId()).isEqualTo(newValue);
}
}
Mocked Method With One or More Arguments
Usually, we have a static method with some arguments (nextMultipleIds
in our case). Then, we need to use a lambda expression instead of the method reference (see line 5). Again, we can use the standard methods (e.g. then
, thenRetun
, thenThrow
etc.) to handle the response with the desired behavior.
@Test
void whenWithArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
int newValuesCount = 5;
seqGeneratorMock.when(() -> SequenceGenerator.nextMultipleIds(newValuesCount))
.thenReturn(List.of(1, 2, 3, 4, 5));
assertThat(SequenceGenerator.nextMultipleIds(newValuesCount)).hasSize(newValuesCount);
}
}
Verify Method Invocation
Similarly, we can also verify calls of the mocked component by calling seqGeneratorMock.verify
method for the method reference (see line 7)
@Test
void verifyUsageWithoutArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
var person = new Person("Pamela");
seqGeneratorMock.verify(SequenceGenerator::nextId);
assertThat(person.getId()).isEqualTo(0);
}
}
Or the lambda expression (see line 6).
@Test
void verifyUsageWithArgument() {
try (MockedStatic<SequenceGenerator> seqGeneratorMock = mockStatic(SequenceGenerator.class)) {
List<Integer> nextIds = SequenceGenerator.nextMultipleIds(3);
seqGeneratorMock.verify(() -> SequenceGenerator.nextMultipleIds(ArgumentMatchers.anyInt()));
assertThat(nextIds).isEmpty();
}
}
Note: please be aware that seqGeneratorMock
doesn't provide any value here, as the static methods are still mocked with the defaults. There's no spy version so far. Therefore, any expected return value has to be mocked, or the default value is returned.
Setup
The mockStatic
feature is enabled in Mockito 5.x by default. Therefore, no special setup is needed. But we need to set up Mockito for the older versions (e.g., 4.x).
Mockito 5.x+
As it was already mentioned, we don't need to set up anything in version 5.x. See the statement in the GitHub repository:
Mockito 5 switches the default mockmaker to mockito-inline, and now requires Java 11.
Old Mockito Versions
When an older version is used, and we use the mock-inline feature via mockStatic
then we can see an error like this:
org.mockito.exceptions.base.MockitoException:
The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks
Mockito's inline mock maker supports static mocks based on the Instrumentation API.
You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.
Note that Mockito's inline mock maker is not supported on Android.
at com.github.aha.poc.junit.person.StaticUsageTests.mockStaticNoArgValue(StaticUsageTests.java:15)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Generally, there are two options to enable it for such Mockito versions (see all Mockito versions here).
Use MockMaker Resource
The first option is based on adding <project>\src\test\resources\mockito-extensions\org.mockito.plugins.MockMaker
to our Maven project with this content:
mock-maker-inline
Use mock-inline
Dependency
The other, and probably better, option is adding mockito-inline
dependency:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
Note: this dependency already contains MockMaker
resource mentioned above. Therefore, this option seems more convenient.
Maven Warning
No matter what version is used (see above), Maven build can produce these warnings:
WARNING: A Java agent has been loaded dynamically (<user_profile>\.m2\repository\net\bytebuddy\byte-buddy-agent\1.12.9\byte-buddy-agent-1.12.9.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
Mockito works correctly even with these warnings. It's probably caused by/depends on the used tool, JDK version, etc.
Conclusion
In this article, the mocking of static methods with the help of Mockito inline mock maker was covered. The article started with the basics of static mocking and then followed with a demonstration of when
and verify
usage (either with the method reference or the lambda expression). In the end, the setup of the Mockito inline maker was shown for different Mockito versions.
The used source code can be found here.
Opinions expressed by DZone contributors are their own.
Comments