Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Стиль 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).

Весь словарь

Обычный MockitoBDDMockito
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, как в этом уроке.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.