Avatar ·

Разница между @Mock и @InjectMocks в Mockito: когда и что использовать?

Всем привет! Я начинающий Java-разработчик, пишу юнит-тесты с использованием Mockito и Spring. У меня возникла путаница с аннотациями @Mock и @InjectMocks. Читал документацию, но до конца не понял, в чем разница и когда что применять. Допустим, у меня есть сервис UserService, который использует UserRepository и EmailService. Я хочу протестировать метод registerUser() в UserService, который вызывает репозиторий для сохранения пользователя и затем отправляет email. Вот пример моего класса: ```java public class UserService { private UserRepository userRepository; private EmailService emailService; public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; } public User registerUser(String name, String email) { User user = new User(name, email); userRepository.save(user); emailService.sendWelcomeEmail(user); return user; } } ``` Я написал тест, но не уверен, правильно ли я использую аннотации: ```java @ExtendWith(MockitoExtension.class) public class UserServiceTest { @Mock private UserRepository userRepository; @Mock private EmailService emailService; @InjectMocks private UserService userService; @Test void testRegisterUser() { // given User user = new User("John", "john@test.com"); when(userRepository.save(any(User.class))).thenReturn(user); // when User result = userService.registerUser("John", "john@test.com"); // then assertNotNull(result); verify(emailService).sendWelcomeEmail(user); } } ``` Вопросы: 1. Почему UserRepository и EmailService помечены @Mock, а UserService — @InjectMocks? 2. Что произойдет, если я поменяю их местами? 3. Нужно ли мне создавать экземпляр UserService вручную через new, если я не использую @InjectMocks? 4. Как быть, если у UserService есть несколько конструкторов? Как Mockito понимает, какой конструктор использовать? Заранее спасибо за разъяснения! <<>>

Основное различие: @Mock создает заглушки, @InjectMocks внедряет их

Давайте разберем на пальцах. Аннотации @Mock и @InjectMocks решают разные задачи:

  • @Mock — создает имитацию (mock) объекта. Вы указываете: "вот этот объект будет поддельным, я сам решу, что он должен возвращать".
  • @InjectMocks — создает реальный объект тестируемого класса и автоматически внедряет в него все mock-объекты, помеченные @Mock (или @Spy).

В вашем примере всё верно:

@Mock
private UserRepository userRepository;  // создаем подделку

@Mock
private EmailService emailService;       // создаем подделку

@InjectMocks
private UserService userService;         // создаем реальный UserService и вставляем в него подделки

Mockito смотрит на конструкторы UserService, находит тот, который принимает UserRepository и EmailService, и передает туда созданные mock-объекты. Вы получаете рабочий UserService, который внутри использует заглушки, а не реальную базу данных или почтовый сервер.

Что будет, если поменять местами?

Если вы напишете @Mock private UserService userService, то весь UserService станет заглушкой. Все его методы будут возвращать значения по умолчанию (null, 0, false), и вы не сможете протестировать реальную логику метода registerUser(). Тест потеряет смысл.

Если же написать @InjectMocks private UserRepository userRepository, то Mockito попытается создать реальный объект UserRepository и внедрить в него... ничего, потому что у UserRepository, скорее всего, нет зависимостей от UserService и EmailService. Это вызовет ошибку или создаст объект с null-полями.

Можно ли обойтись без @InjectMocks?

Да, можно создать объект вручную:

@Mock
private UserRepository userRepository;

@Mock
private EmailService emailService;

private UserService userService;

@BeforeEach
void setUp() {
    userService = new UserService(userRepository, emailService);
}

Это абсолютно равнозначный подход. @InjectMocks — просто синтаксический сахар, который избавляет от написания такого кода. Но у него есть один нюанс: если у класса несколько конструкторов, Mockito выберет самый большой (с наибольшим количеством параметров). Если вам нужен другой конструктор — лучше создать объект вручную.

Почему @InjectMocks — это не всегда хорошо?

  1. Неявное поведение. Если вы добавите новую зависимость в конструктор UserService, тест может сломаться, потому что Mockito не сможет найти подходящий mock.
  2. Проблемы с полями. @InjectMocks также пытается внедрить зависимости через поля (если нет подходящего конструктора). Это может привести к неожиданностям, если у класса есть private-поля без сеттеров.
  3. Сложность отладки. При ручном создании объекта вы точно знаете, какие зависимости передаются. При @InjectMocks — полагаетесь на "магию" Mockito.

Рекомендация

Для простых случаев (один конструктор, все зависимости — mock-объекты) используйте @InjectMocks — это сокращает код. Если у вас сложная иерархия или несколько конструкторов — создавайте объект вручную через new. И никогда не ставьте @Mock на тестируемый класс — это делает тест бесполезным.

<<>>

Альтернативный взгляд: используйте конструктор без @InjectMocks

Лично я предпочитаю не использовать @InjectMocks в продакшн-коде. Вот почему:

  • Явность лучше неявности. Когда я пишу new UserService(userRepoMock, emailMock), я сразу вижу, какие зависимости передаются. Если тест падает, я точно знаю, что передал.
  • Контроль над конструктором. @InjectMocks может выбрать неправильный конструктор, если их несколько. При ручном создании вы явно указываете, какой конструктор вызывать.
  • Лучшая поддержка рефакторинга. Если вы переименуете параметр в конструкторе, @InjectMocks может сломаться (он ищет совпадение по типу, а не по имени). Ручное создание этого недостатка лишено.

Пример предпочтительного подхода:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    @Mock
    private EmailService emailService;
    
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        userService = new UserService(userRepository, emailService);
    }
    
    // тесты...
}

Этот код чуть длиннее, но он более прозрачный и устойчивый к изменениям. @InjectMocks хорош для быстрых прототипов и обучения, но для серьезных проектов я рекомендую ручное связывание.