Сопоставители аргументов и ArgumentCaptor
До сих пор мы передавали в when и verify точные значения: findById(1L), send("anna", "..."). Часто точное значение неизвестно или неважно: «на любой id верни этот пост», «в сообщении должен быть заголовок». Для этого в Mockito есть сопоставители аргументов (argument matchers). А когда нужно посмотреть на объект, который сервис создал внутри себя, его перехватывает ArgumentCaptor.
Сопоставители
Сопоставители — статические методы класса org.mockito.ArgumentMatchers:
| Сопоставитель | Подходит |
|---|---|
any(), any(Post.class) | Любой объект, любой Post |
anyLong(), anyInt(), anyString() | Любое значение типа (для anyString — не null) |
eq(value) | Ровно это значение |
contains("текст"), startsWith("текст") | Строка с фрагментом |
isNull(), notNull() | null / не null |
argThat(p -> ...) | Ваше собственное условие |
// src/test/java/com/example/blog/PostServiceMatchersTest.java
@Test
void anyIdReturnsTheSamePost() {
when(repository.findById(anyLong()))
.thenReturn(Optional.of(new Post(5L, "Привет", "anna", false)));
assertEquals("Привет", service.find(1L).title());
assertEquals("Привет", service.find(42L).title());
}
@Test
void notificationMentionsTitle() {
when(repository.findById(1L))
.thenReturn(Optional.of(new Post(1L, "Привет", "anna", false)));
service.publish(1L);
verify(notifier).send(eq("anna"), contains("Привет"));
verify(notifier, never()).send(eq("oleg"), anyString());
}Первый тест настраивает репозиторий на любой id. Второй проверяет, что уведомление ушло Анне и упоминает заголовок, не повторяя весь текст. Подготовка та же, что в прошлом уроке: repository, notifier и service — поля класса.
Ответ зависит от аргумента: thenAnswer
Настоящий репозиторий возвращает сохранённый пост с новым id. thenAnswer вычисляет результат по аргументам вызова:
// src/test/java/com/example/blog/PostServiceMatchersTest.java
when(repository.save(any(Post.class)))
.thenAnswer(invocation -> {
Post post = invocation.getArgument(0);
return new Post(1L, post.title(), post.author(), post.published());
});invocation.getArgument(0) — первый аргумент, переданный в save().
ArgumentCaptor
create() собирает объект Post внутри сервиса. Чтобы проверить его поля, перехватим его на пути в репозиторий:
// src/test/java/com/example/blog/PostServiceMatchersTest.java
@Test
void createSavesTrimmedDraft() {
when(repository.save(any(Post.class)))
.thenAnswer(invocation -> {
Post post = invocation.getArgument(0);
return new Post(1L, post.title(), post.author(), post.published());
});
service.create(" Привет ", "anna");
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
verify(repository).save(captor.capture());
Post saved = captor.getValue();
assertNull(saved.id());
assertEquals("Привет", saved.title());
assertFalse(saved.published());
}captor.capture() внутри verify работает как сопоставитель, который принимает любой аргумент и запоминает его. getValue() возвращает последнее перехваченное значение, getAllValues() — все, если вызовов было несколько. Тест проверяет, что сервис сохранил черновик без id и с обрезанным заголовком.
Частая ошибка
Если в одном аргументе стоит сопоставитель, сопоставителями должны быть все аргументы этого вызова. Смешивание с обычными значениями:
verify(notifier).send("anna", anyString());org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at com.example.blog.MatchersMixTest.mixed(MatchersMixTest.java:23)
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.Оберните обычное значение в eq(): verify(notifier).send(eq("anna"), anyString()).