Avatar ·

Как заставить мок-метод возвращать переданный ему аргумент в Mockito?

Всем привет! Я новичок в Java и Mockito, и столкнулся с проблемой при написании тестов. У меня есть класс, который вызывает метод репозитория для сохранения объекта, а затем использует возвращённое значение (например, сгенерированный ID). Я хочу замокать этот репозиторий, чтобы метод возвращал тот же объект, который был передан, но с некоторыми изменениями (например, установленным ID). Как это сделать? Вот пример кода, который я тестирую: ```java public class UserService { private UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User createUser(String name, String email) { User user = new User(name, email); User savedUser = userRepository.save(user); // savedUser должен иметь ID, установленный базой данных return savedUser; } } ``` Мой тест выглядит так: ```java @ExtendWith(MockitoExtension.class) public class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test public void testCreateUser() { User user = new User("John", "john@example.com"); when(userRepository.save(any(User.class))).thenReturn(user); User result = userService.createUser("John", "john@example.com"); assertEquals("John", result.getName()); assertNotNull(result.getId()); // Ошибка! ID равен null } } ``` Проблема в том, что `userRepository.save()` возвращает тот же объект `user`, который я создал в тесте, но без ID. В реальном коде ID генерируется базой данных. Как настроить мок, чтобы он возвращал переданный аргумент, но с установленным ID (например, 123)? Я слышал про `Answer`, но не понимаю, как его правильно использовать. Помогите, пожалуйста! <<>>

Решение: использование Answer для возврата модифицированного аргумента

Проблема в том, что thenReturn() возвращает фиксированный объект, который вы передаёте. Чтобы вернуть переданный аргумент с изменениями, нужно использовать Answer — интерфейс, который позволяет выполнять произвольный код при вызове метода мока.

В вашем случае нужно создать реализацию Answer, которая:

  • Извлекает аргумент, переданный в метод (первый и единственный аргумент типа User).
  • Устанавливает ему ID (например, через сеттер или рефлексию).
  • Возвращает модифицированный объект.

Пример кода с использованием Answer

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

@Test
public void testCreateUser() {
    User userInput = new User("John", "john@example.com");
    
    when(userRepository.save(any(User.class))).thenAnswer(new Answer<User>() {
        @Override
        public User answer(InvocationOnMock invocation) throws Throwable {
            // Получаем первый аргумент (переданный User)
            User arg = invocation.getArgument(0);
            // Устанавливаем ID (например, 123)
            arg.setId(123L);
            return arg;
        }
    });
    
    User result = userService.createUser("John", "john@example.com");
    assertEquals("John", result.getName());
    assertEquals(Long.valueOf(123), result.getId()); // Теперь работает!
}

Более краткая версия с лямбдой (Java 8+)

@Test
public void testCreateUser() {
    when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
        User arg = invocation.getArgument(0);
        arg.setId(123L);
        return arg;
    });
    
    User result = userService.createUser("John", "john@example.com");
    assertEquals("John", result.getName());
    assertEquals(Long.valueOf(123), result.getId());
}

Важные моменты

  • Изменение оригинального объекта: В этом примере мы модифицируем объект, который был передан в мок. Это может повлиять на другие части теста, если вы используете тот же объект позже. Если нужно избежать мутации, создавайте копию.
  • Генерация ID: В реальном приложении ID генерируется базой данных. В тесте вы можете использовать счётчик или случайное число для имитации.
  • Использование с несколькими аргументами: Если метод принимает несколько аргументов, используйте invocation.getArgument(index) с нужным индексом.

Альтернатива: использование thenAnswer с method.getArgumentAt()

Если вы используете старую версию Mockito или хотите явно указать тип, можно использовать invocation.getArgumentAt(0, User.class) (устарело в новых версиях, используйте getArgument(0)).

when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
    User arg = invocation.getArgumentAt(0, User.class); // для старых версий
    arg.setId(123L);
    return arg;
});

Полный рабочий пример

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 static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    @InjectMocks
    private UserService userService;

    @Test
    public void testCreateUser() {
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
            User user = invocation.getArgument(0);
            user.setId(123L);
            return user;
        });
        
        User result = userService.createUser("John", "john@example.com");
        assertEquals("John", result.getName());
        assertEquals(Long.valueOf(123), result.getId());
    }
}

Теперь ваш тест будет работать, и мок будет возвращать объект с установленным ID, как если бы это сделала база данных.

<<>>

Дополнительный подход: использование Mockito.spy для частичного мокирования

Если ваш UserRepository — это реальный класс (не интерфейс), и вы хотите сохранить часть его поведения, можно использовать spy вместо mock. Это позволит вызвать реальный метод save(), но с возможностью переопределить его части.

Однако для вашей задачи это менее удобно, чем Answer, потому что:

  • Spy требует реального экземпляра репозитория (например, с подключением к тестовой БД).
  • Вы всё равно не сможете легко сгенерировать ID, не вызывая реальную БД.

Лучше использовать Answer, как описано в первом ответе. Но если вам нужно просто вернуть переданный аргумент без изменений (например, для методов, которые не меняют объект), можно использовать встроенный AdditionalAnswers.returnsFirstArg() (доступен в Mockito 2.x+):

import static org.mockito.AdditionalAnswers.returnsFirstArg;

when(userRepository.save(any(User.class))).then(returnsFirstArg());

Этот метод возвращает первый аргумент как есть, без модификаций. Если нужно модифицировать, используйте Answer с лямбдой.