Utilitest is a collection of test utilities that provides integration support for popular testing libraries like AssertJ, Mockito, and Awaitility.
Go ahead and add the latest version of the library to your project:
<dependency>
<groupId>io.github.etrandafir93</groupId>
<artifactId>utilitest</artifactId>
<version>${version}</version>
<scope>test</scope>
</dependency>
To use verify() mocks, we generally have two options. We can either capture the arguments using a Captor, which adds some boilerplate code and overhead, or we can use Mockito's ArgumentMatchers to verify the arguments directly with a lambda:
@Test
void customAssertMatcher() {
FooService mock = Mockito.mock();
mock.process(new Account(1L, "John Doe", "[email protected]"));
Mockito.verify(mock).process(
Mockito.argThat(
it -> it.getAccountId().equals(1L)
&& it.getName().equals("John Doe")
&& it.getEmail().equals("[email protected]")));
}
However, the failure messages from these custom argument matchers are often cryptic, making it difficult to pinpoint the cause of the test failure:
Argument(s) are different! Wanted:
fooService.process(
<custom argument matcher>
);
-> at io.github.etr.utilitest.ReadmeExampelsTest$FooService.process(ReadmeExampelsTest.java:26)
Actual invocations have different arguments:
fooService.process(
io.github.etr.utilitest.ReadmeExampelsTest$Account@245a26e1
);
-> at io.github.etr.utilitest.ReadmeExampelsTest.customAssertMatcher(ReadmeExampelsTest.java:62)
As a workaround, we can use a fluent AssertJ assertion within the custom ArgumentMatcher and always return true:
@Test
void assertMatcherWithAssertJ() {
FooService mock = Mockito.mock();
mock.process(new Account(1L, "John Doe", "[email protected]"));
Mockito.verify(mock).process(
Mockito.argThat(it -> {
Assertions.assertThat(it)
.hasFieldOrPropertyWithValue("accountId", 1L)
.hasFieldOrPropertyWithValue("name", "John Doe")
.hasFieldOrPropertyWithValue("email", "[email protected]");
return true;
}));
}
While this solution provides much clearer error messages, it comes with the downside of adding a lot of boilerplate code. If the code is repetitive, it can make the tests harder to read and maintain.
So, let's remove all this ceremony and use utilitest's MockitoAndAssertJ::argThat
instead:
Mockito.verify(mock).process(
MockitoAndAssertJ.argThat(it -> it
.hasFieldOrPropertyWithValue("accountId", 1L)
.hasFieldOrPropertyWithValue("name", "John Doe")
.hasFieldOrPropertyWithValue("email", "[email protected]")));
This approach brings together the best of both worlds: the convenience of verifying the argument using a lambda expression and the fluent API of AssertJ, providing clear and descriptive error messages:
java.lang.AssertionError:
Expecting
io.github.etr.utilitest.ReadmeExampelsTest$Account@f5c79a6
to have a property or a field named "email" with value
"[email protected]"
but value was:
"[email protected]"
The MockitoAndAssertJ::argThat
from the previous example enables us to consume an ObjectAssert from AsserJ, that provides some basic assertions. The assertJ API allows us to change this type to a more specialized instance of assertion, to verify specific properties. For example, we can change the assertion type to a MapAssert to be able to check speciifc key-value entries:
@Test
void asInstanceOf() {
Map<Long, String> data = Map.of(
1L, "John",
2L, "Bobby"
);
FooService mock = Mockito.mock();
mock.processMap(data);
verify(mock).processMap(
MockitoAndAssertJ.argThat(it -> it
.asInstanceOf(InstanceOfAssertFactories.MAP)
.containsEntry(1L, "John")
.containsEntry(2L, "Bobby")));
}
With MockitoAndAssertJ, we can also specify InstanceOfAssertFactories upfront. To achieve this, we split the argThat into two separate methods: MockitoAndAssertJ.arg(InstanceOfAssertFactories.MAP).that(it -> ...)
.
Let's use this API to verify a method that accepts a LocalDateTime and a List of Strings:
@Test
void arg_that() {
FooService mock = Mockito.mock();
mock.processDateAndList(now(), List.of("A", "B", "C"));
verify(mock).processDateAndList(
arg(TEMPORAL).that(time -> time.isCloseTo(now(), within(500, MILLIS))),
arg(LIST).that(list -> list.containsExactly("A", "B", "C"))
);
}