Стиль BDD: given, willReturn, then, should
BDD (behavior-driven development, разработка через поведение) описывает поведение в три части: given — дана ситуация, when — что-то происходит, then — ожидаем результат. Это те же «подготовка, действие, проверка», только другими словами. Беда обычного Mockito в том, что часть «дано» начинается с метода when(...), и это сбивает при чтении. Класс BDDMockito даёт те же возможности с подходящими именами: given(...).willReturn(...) вместо when(...).thenReturn(...) и then(mock).should() вместо verify(mock). BDDMockito входит в Mockito — в pom.xml ничего добавлять не нужно.
given и then
Создайте src/test/java/com/example/blog/PostServiceBddTest.java:
// src/test/java/com/example/blog/PostServiceBddTest.java
package com.example.blog;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.*;
@ExtendWith(MockitoExtension.class)
class PostServiceBddTest {
@Mock
PostRepository repository;
@Mock
Notifier notifier;
@InjectMocks
PostService service;
@Test
void givenDraft_whenPublish_thenAuthorIsNotified() {
// given
given(repository.findById(1L))
.willReturn(Optional.of(new Post(1L, "Привет", "anna", false)));
// when
Post post = service.publish(1L);
// then
assertThat(post.published()).isTrue();
then(notifier).should().send("anna", "Ваш пост «Привет» опубликован");
}
}Три комментария делят тест на части, и в каждой части своё слово. then(notifier).should() — то же самое, что verify(notifier).
Весь словарь
| Обычный Mockito | BDDMockito |
|---|---|
when(mock.find()).thenReturn(x) | given(mock.find()).willReturn(x) |
when(...).thenThrow(e) | given(...).willThrow(e) |
doThrow(e).when(mock).send(...) | willThrow(e).given(mock).send(...) |
verify(mock).save(...) | then(mock).should().save(...) |
verify(mock, never()).save(...) | then(mock).should(never()).save(...) |
verifyNoInteractions(mock) | then(mock).shouldHaveNoInteractions() |
// src/test/java/com/example/blog/PostServiceBddTest.java
@Test
void givenNoPost_whenPublish_thenNothingIsSaved() {
given(repository.findById(99L)).willReturn(Optional.empty());
assertThatThrownBy(() -> service.publish(99L))
.isInstanceOf(PostNotFoundException.class);
then(repository).should(never()).save(any(Post.class));
then(notifier).shouldHaveNoInteractions();
}
@Test
void givenMailIsDown_whenPublish_thenErrorIsPassedOn() {
given(repository.findById(1L))
.willReturn(Optional.of(new Post(1L, "Привет", "anna", false)));
willThrow(new IllegalStateException("Почтовый сервер недоступен"))
.given(notifier).send(any(), any());
assertThatThrownBy(() -> service.publish(1L))
.hasMessage("Почтовый сервер недоступен");
}[INFO] Running com.example.blog.PostServiceBddTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.788 s -- in com.example.blog.PostServiceBddTestКакой стиль выбрать — дело вкуса. Выберите один на проект и не смешивайте when и given в одном классе.
Названия: given, when, then
Названия строятся по той же идее: givenDraft_whenPublish_thenAuthorIsNotified. Подчёркивания разделяют части, и падение в отчёте уже само рассказывает историю. Другие популярные формы — publish_sendsNotification_whenDraftExists или @DisplayName("черновик после публикации уведомляет автора"). В любой форме название должно содержать ситуацию и ожидаемый результат, а не только имя метода.
Аннотация @Spy
В уроке о шпионах они создавались через spy(...). С MockitoExtension то же делает аннотация @Spy, а @InjectMocks передаёт шпиона в сервис, как любой мок:
// src/test/java/com/example/blog/SpyAnnotationTest.java
@ExtendWith(MockitoExtension.class)
class SpyAnnotationTest {
@Spy
InMemoryPostRepository repository;
@Mock
Notifier notifier;
@InjectMocks
PostService service;
@Test
void givenRealRepository_whenCreate_thenPostIsStored() {
Post post = service.create("Привет", "anna");
assertThat(repository.findAll()).containsExactly(post);
then(repository).should().save(any(Post.class));
}
}Mockito сам создаёт new InMemoryPostRepository() и оборачивает его в шпиона; настоящий save() сохраняет пост. Если конструктору нужны аргументы, можно написать @Spy InMemoryPostRepository repository = new InMemoryPostRepository(); с готовым объектом. @Spy нужен настоящий класс: у интерфейса вроде PostRepository нет кода, который можно вызвать, и каждый метод вернёт пустое значение.
Частая ошибка
В AssertJ есть свой BDD-класс BDDAssertions с методом then(значение). Если в одном файле оба импорта, компилятор не может различить два then:
import static org.assertj.core.api.BDDAssertions.*;
import static org.mockito.BDDMockito.*;[ERROR] /.../src/test/java/com/example/blog/ThenClashTest.java:[34,9] reference to then is ambiguous
both method <T>then(T) in org.assertj.core.api.BDDAssertions and method <T>then(T) in org.mockito.BDDMockito match
[ERROR] /.../src/test/java/com/example/blog/ThenClashTest.java:[34,23] cannot find symbol
symbol: method should()
location: class org.assertj.core.api.ObjectAssert<com.example.blog.Notifier>Оставьте org.mockito.BDDMockito.* для моков, а значения проверяйте через assertThat из org.assertj.core.api.Assertions, как в этом уроке.