Avatar ·

Как правильно мокать (mock) void методы в Mockito?

Всем привет! Я начинающий Java-разработчик, пишу unit-тесты с использованием Mockito. Столкнулся с проблемой: не могу понять, как правильно замокать метод, который ничего не возвращает (void).

Вот простой пример. Есть класс UserService, который отправляет email через EmailService:

public class EmailService {
    public void sendEmail(String to, String subject, String body) {
        // реальная отправка email
        System.out.println("Email sent to " + to);
    }
}

public class UserService {
    private final EmailService emailService;
    
    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }
    
    public void registerUser(String email) {
        // какая-то логика регистрации
        emailService.sendEmail(email, "Welcome", "Hello!");
    }
}

Я пишу тест для registerUser и хочу проверить, что sendEmail был вызван. Но не понимаю, как замокать void-метод. Когда я пытаюсь использовать when(), компилятор ругается:

@Test
public void testRegisterUser() {
    EmailService mockEmail = mock(EmailService.class);
    UserService userService = new UserService(mockEmail);
    
    // Это не работает!
    when(mockEmail.sendEmail(anyString(), anyString(), anyString())).thenReturn(...);
    
    userService.registerUser("test@test.com");
    verify(mockEmail).sendEmail("test@test.com", "Welcome", "Hello!");
}

Ошибка: when() требует возвращаемого значения, а void-метод ничего не возвращает. Как правильно замокать такой метод? И как проверить, что он был вызван с правильными аргументами?

Заранее спасибо за помощь!

<<>>

Основной подход: doNothing() и verify()

В Mockito для void-методов используется другой синтаксис. Вместо when() применяется doNothing() (или doThrow() для исключений), а для проверки вызова — verify(). Вот правильное решение для вашего случая:

import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;

public class UserServiceTest {
    
    @Test
    public void testRegisterUser() {
        // 1. Создаём мок
        EmailService mockEmail = mock(EmailService.class);
        UserService userService = new UserService(mockEmail);
        
        // 2. Настраиваем поведение void-метода (опционально)
        doNothing().when(mockEmail).sendEmail(anyString(), anyString(), anyString());
        
        // 3. Вызываем тестируемый метод
        userService.registerUser("test@test.com");
        
        // 4. Проверяем, что метод был вызван с правильными аргументами
        verify(mockEmail).sendEmail("test@test.com", "Welcome", "Hello!");
    }
}

Ключевые моменты:

  • doNothing() — говорит Mockito "ничего не делай при вызове этого void-метода". Это поведение по умолчанию для моков, поэтому его можно опустить, если не нужно генерировать исключение.
  • verify() — проверяет, что метод был вызван хотя бы один раз с указанными аргументами. Можно уточнить количество вызовов: verify(mock, times(2)), verify(mock, never()) и т.д.

Дополнительные возможности:

1. Имитация исключения (doThrow)

// Метод выбросит исключение при вызове
doThrow(new RuntimeException("Ошибка отправки"))
    .when(mockEmail).sendEmail(anyString(), anyString(), anyString());

// Проверяем, что исключение обрабатывается
assertThrows(RuntimeException.class, () -> {
    userService.registerUser("test@test.com");
});

2. Проверка порядка вызовов (InOrder)

InOrder inOrder = inOrder(mockEmail);
inOrder.verify(mockEmail).sendEmail("a@a.com", "Subj1", "Body1");
inOrder.verify(mockEmail).sendEmail("b@b.com", "Subj2", "Body2");

3. Проверка аргументов с ArgumentCaptor

Если нужно проверить сложные аргументы:

ArgumentCaptor<String> toCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> subjectCaptor = ArgumentCaptor.forClass(String.class);

verify(mockEmail).sendEmail(toCaptor.capture(), subjectCaptor.capture(), anyString());

assertEquals("test@test.com", toCaptor.getValue());
assertEquals("Welcome", subjectCaptor.getValue());

Важные замечания:

  • Для void-методов нельзя использовать when().thenReturn() — это синтаксическая ошибка.
  • Если мок создан через @Mock аннотацию, синтаксис остаётся тем же.
  • Для методов, возвращающих void, можно также использовать doAnswer(), если нужно выполнить какое-то действие при вызове.

Надеюсь, теперь стало понятнее! Главное правило: void-методы мокаются через doNothing/doThrow/doAnswer, а проверяются через verify.

<<>>

Альтернативный подход: Использование doAnswer() для кастомного поведения

Иногда нужно не просто заглушить void-метод, а выполнить какую-то логику при его вызове. Например, изменить состояние объекта или записать аргументы. В таких случаях помогает doAnswer():

// Создаём список для отслеживания вызовов
List<String> sentEmails = new ArrayList<>();

doAnswer(invocation -> {
    String to = invocation.getArgument(0);
    String subject = invocation.getArgument(1);
    String body = invocation.getArgument(2);
    
    // Сохраняем информацию о вызове
    sentEmails.add(to + " - " + subject);
    
    // Возвращаем null, так как метод void
    return null;
}).when(mockEmail).sendEmail(anyString(), anyString(), anyString());

// Вызываем тестируемый метод
userService.registerUser("test@test.com");

// Проверяем, что email был "отправлен"
assertEquals(1, sentEmails.size());
assertTrue(sentEmails.get(0).contains("test@test.com"));

Когда это полезно:

  • Нужно проверить не только факт вызова, но и порядок обработки аргументов.
  • Метод void изменяет внешнее состояние (например, базу данных), и вы хотите это смоделировать.
  • Хотите добавить логирование или подсчёт вызовов без создания реального сервиса.

Обратите внимание: doAnswer() работает как с void, так и с возвращающими значение методами. Для void-методов лямбда должна возвращать null.