Encodera

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

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

Оглавление

Моки: mock, when, thenReturn

PostService зависит от репозитория и уведомителя. В настоящем приложении это база данных и почтовый сервер. Модульному тесту не нужно ни то ни другое: это было бы медленно, ненадёжно, и тест рассылал бы настоящие письма. До сих пор мы подменяли зависимости вручную — репозиторием в памяти и лямбдой. Mockito создаёт такие заглушки автоматически.

Мок (mock) — объект, который реализует интерфейс, но сам ничего не делает. Вы говорите ему, что вернуть на какой вызов (это называется стабинг, stubbing), а потом можете спросить, какие вызовы он получил.

Подключение Mockito

Добавьте в dependencies файла pom.xml:

<!-- pom.xml -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>5.24.0</version>
  <scope>test</scope>
</dependency>

mockito-junit-jupiter подтягивает mockito-core и интеграцию с JUnit. На Java 21 и новее при первом тесте Mockito печатает предупреждение:

Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: ...
WARNING: A Java agent has been loaded dynamically (...\byte-buddy-agent-1.17.7.jar)

Mockito меняет классы в памяти через Java-агент, а будущие JDK запретят подключать агент на лету. Документация Mockito рекомендует передавать агент тестовой JVM явно:

<!-- pom.xml -->
<properties>
  <!-- ... -->
  <argLine></argLine>
</properties>
 
<build>
  <plugins>
    <plugin>
      <artifactId>maven-dependency-plugin</artifactId>
      <version>3.11.0</version>
      <executions>
        <execution>
          <goals>
            <goal>properties</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.6.0</version>
      <configuration>
        <argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
      </configuration>
    </plugin>
  </plugins>
</build>

Цель properties создаёт для каждой зависимости свойство с путём к её jar-файлу, так что ${org.mockito:mockito-core:jar} превращается в путь к Mockito. @{argLine} сохраняет аргументы, которые добавляют другие плагины (это понадобится позже для покрытия), а пустое свойство argLine делает запись корректной, когда таких аргументов нет. -Xshare:off убирает ещё одно предупреждение JVM о совместном использовании классов. После этого предупреждения пропадают.

Первый мок

// src/test/java/com/example/blog/PostServiceMockTest.java
package com.example.blog;
 
import org.junit.jupiter.api.Test;
 
import java.util.Optional;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
 
class PostServiceMockTest {
 
    @Test
    void findReturnsPostFromRepository() {
        PostRepository repository = mock(PostRepository.class);
        Notifier notifier = mock(Notifier.class);
        when(repository.findById(1L))
                .thenReturn(Optional.of(new Post(1L, "Привет, Mockito", "anna", false)));
        PostService service = new PostService(repository, notifier);
 
        Post post = service.find(1L);
 
        assertEquals("Привет, Mockito", post.title());
    }
}

mock(PostRepository.class) создаёт заглушку. when(repository.findById(1L)).thenReturn(...) читается как «когда вызовут findById(1L), верни этот пост». Сервис не знает, что разговаривает с моком: он просто вызывает интерфейс.

Что возвращает мок без настройки

Методы, которым вы не задали ответ, возвращают «пустые» значения:

// src/test/java/com/example/blog/PostServiceMockTest.java
System.out.println(repository.findById(7L));
System.out.println(repository.findAll());
System.out.println(repository.save(new Post(null, "X", "anna", false)));
Optional.empty
[]
null

Optional → Optional.empty, коллекции → пустые, числа → 0, boolean → false, остальные объекты → null. Поэтому service.find(7L) на таком моке выбросит PostNotFoundException — это тоже стоит проверить тестом.

Частая ошибка

when() работает только с вызовом на моке. Если по ошибке передать настоящий объект:

PostRepository repository = new InMemoryPostRepository();
when(repository.findById(1L)).thenReturn(...);
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Создайте объект через mock(...). Та же ошибка возникает, если метод private или static, а также для equals() и hashCode() — их Mockito через when подменить не может.

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

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