Привет, Хабр!

герой нашей статьи
герой нашей статьи

Помню, как в одном из моих первых проектов, мы столкнулись с проблемой тестирования кода, который зависел от внешних сервисов и баз данных. Это было не только трудоемко, но и занимало уйму времени. И сразу стало понятно, что без мокирования не обойтись. Мокирование создать поддельные версии сложных объектов или систем, чтобы мы могли тестировать каждую часть нашего кода изолированно. Это упрощает написание тестов и бустит их выполнение.

Несколько строк кода, и вы уже создаете моки для ваших сервисов и DAO! Mockito предоставляет понятный API для мокирования, который делает тесты не только проще в написании, но и более читабельными.

Установка

Первое, что нам нужно сделать, это интегрировать Mockito в наш проект. Если вы работаете с Maven, то в ваш pom.xml необходимо добавить следующую зависимость:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.x.x</version>
    <scope>test</scope>
</dependency>

Для Gradle это будет выглядеть так:

testImplementation 'org.mockito:mockito-core:5.x.x'

5.x.x - это место для актуальной версии Mockito. Рекомендую всегда проверять последнюю версию на официальном сайте Mockito. На момент статьи версия 5.8.0.

После того, как зависимости добавлены, убедитесь, что ваша IDE корректно обновила Если вы новичок в тестировании, будет полезно знать, что Mockito идеально работает с JUnit. Обычно, нет необходимости в дополнительной настройке, но чекните, что JUnit также добавлен в ваш проект.

На мой взгляд JUnit самый лучший фрейм для мокито.

JUnit используется для структурирования тестов и выполнения утверждений (assertions), в то время как Mockito предоставляет механизмы для создания моков и шпионов, которые необходимы для изоляции тестируемого кода от его зависимостей.

Mockito тесно интегрирован с JUnit. Например, с помощью аннотации @RunWith(MockitoJUnitRunner.class) в JUnit 4 или @ExtendWith(MockitoExtension.class) в JUnit 5 можно инициализировать моки, созданные с помощью аннотации @Mock, автоматически.

Совместное использование JUnit и Mockito упрощает тестирование. Mockito позволяет создавать моки для служб, репозиториев и других компонентов, таким образом, что тесты могут сосредоточиться на логике, не заботясь о внешних зависимостях, таких как различные бдшки и тп.

Основы Mockito

Для начала, создадим простой мок-объект. Представим, у нас есть интерфейс DataService, который мы хотим замокировать:

public interface DataService {
    double retrieveData();
}

Используя Mockito, мы можем создать мок этого интерфейса очень просто:

DataService mockDataService = Mockito.mock(DataService.class);

mockDataService - это мок-объект, который можно использовать в тестах. Он будет иметь поведение по умолчанию для всех методов (например, возвращать 0 для методов, возвращающих double).

Чтобы мок был полезен, нам нужно определить его поведение. Допустим, мы хотим, чтобы метод retrieveData() возвращал определенное значение. Сделать это можно следующим образом:

Mockito.when(mockDataService.retrieveData()).thenReturn(15.0);

Теперь при вызове retrieveData() на мок-объекте mockDataService, он вернет 15.0.

Теперь, когда у нас есть мок и мы настроили его поведение, мы можем использовать его в нашем тесте. Допустим, у нас есть класс DataProcessor, который зависит от DataService. Мы можем использовать мок DataService для тестирования DataProcessor:

public class DataProcessor {
    private DataService dataService;

    public DataProcessor(DataService dataService) {
        this.dataService = dataService;
    }

    public double processData() {
        return dataService.retrieveData() * 2;
    }
}

В тесте:

@Test
public void testProcessData() {
    DataService mockDataService = Mockito.mock(DataService.class);
    Mockito.when(mockDataService.retrieveData()).thenReturn(15.0);

    DataProcessor processor = new DataProcessor(mockDataService);
    double result = processor.processData();

    assertEquals(30.0, result, 0.01);
}

Проверяем, что метод processData() корректно удваивает значение, полученное от DataService.

Методы стаббинга в Mockito

Метод when-thenReturn - это, на мой взгляд, самая часто используемая форма стаббинга в Mockito. Он позволяет определить возвращаемое значение для метода мок-объекта.

Допустим, у нас есть интерфейс CalculatorService:

public interface CalculatorService {
    double add(double input1, double input2);
}

Мы можем создать мок для этого интерфейса и определить поведение метода add таким образом:

@Test
public void whenThenReturnExample() {
    CalculatorService calculatorService = Mockito.mock(CalculatorService.class);

    // Стаббинг: определение поведения
    Mockito.when(calculatorService.add(10.0, 20.0)).thenReturn(30.0);

    // Проверка: метод add возвращает 30.0
    assertEquals(30.0, calculatorService.add(10.0, 20.0), 0.01);
}

Здесь мы задаем, что при вызове add(10.0, 20.0) мок должен вернуть 30.0.

В то время как when-thenReturn идеально подходит для большинства сценариев, иногда мы сталкиваемся с некоторыми ограничительными нюансами, особенно когда работаем с методами, которые возвращают void. В этих случаях полезен doReturn-when.

Предположим, у нас есть метод void printSum(double input1, double input2), который не возвращает значение, но мы хотим удостовериться, что он был вызван с определенными параметрами. Мы можем использовать doReturn-when для этой цели:

@Test
public void doReturnWhenExample() {
    CalculatorService calculatorService = Mockito.mock(CalculatorService.class);

    // Стаббинг с использованием doReturn-when
    Mockito.doNothing().when(calculatorService).printSum(10.0, 20.0);

    // Вызов метода
    calculatorService.printSum(10.0, 20.0);

    // Верификация: метод был вызван с заданными параметрами
    Mockito.verify(calculatorService).printSum(10.0, 20.0);
  }

Используем doNothing(), чтобы указать, что при вызове printSum(10.0, 20.0) ничего не должно происходить, но мы можем проверить, что вызов действительно был сделан.

Верификация вызовов методов в Mockito

Допустим, у нас есть интерфейс OrderService, который мы хотим протестировать:

public interface OrderService {
    void placeOrder(Order order);
    int getOrderCount();
}

Когда мы пишем тест, нам может понадобиться убедиться, что метод placeOrder был действительно вызван. Вот как мы можем это сделать с помощью Mockito:

@Test
public void verifyMethodCall() {
    // Создание мока
    OrderService orderService = Mockito.mock(OrderService.class);

    // Создание тестового заказа
    Order testOrder = new Order("TestItem", 3);

    // Имитация вызова метода
    orderService.placeOrder(testOrder);

    // Верификация: был ли вызван метод placeOrder с testOrder
    Mockito.verify(orderService).placeOrder(testOrder);
}

Здесь мы не только создали мок OrderService, но и проверили, что метод placeOrder был вызван с определенным заказом.

Mockito позволяет нам идти еще дальше: мы можем проверить количество вызовов метода и точные параметры, с которыми он был вызван. Это полезно, когда один и тот же метод вызывается несколько раз с разными параметрами.

@Test
public void verifyNumberOfMethodCalls() {
    OrderService orderService = Mockito.mock(OrderService.class);

    // Имитация вызовов
    orderService.placeOrder(new Order("Item1", 1));
    orderService.placeOrder(new Order("Item2", 2));

    // Верификация: метод placeOrder был вызван дважды
    Mockito.verify(orderService, Mockito.times(2)).placeOrder(Mockito.any(Order.class));

    // Верификация: метод getOrderCount ни разу не был вызван
    Mockito.verify(orderService, Mockito.never()).getOrderCount();
}

В этом примере мы проверяем, что placeOrder был вызван ровно два раза с любыми заказами и что getOrderCount не был вызван вообще.

Идем дальше

Мокирование исключений

Мокирование исключений полезно, когда вы хотите тестировать поведение вашего кода в условиях возникновения исключения.

Допустим, у нас есть интерфейс FileReader, который читает файл и может выбрасывать IOException:

public interface FileReader {
    String readFile(String path) throws IOException;
}

Теперь, представим, что мы хотим протестировать поведение нашего класса FileProcessor, который зависит от FileReader, в ситуации, когда IOException выбрасывается.

public class FileProcessor {
    private FileReader fileReader;

    public FileProcessor(FileReader fileReader) {
        this.fileReader = fileReader;
    }

    public String processFile(String path) {
        try {
            return "Processed: " + fileReader.readFile(path);
        } catch (IOException e) {
            return "Error";
        }
    }
}

Чтобы протестировать этот случай, мы можем использовать Mockito для мокирования IOException:

@Test
public void whenIOException_thenReturnsError() throws IOException {
    // Создание мока
    FileReader mockFileReader = Mockito.mock(FileReader.class);

    // Мокирование исключения
    Mockito.when(mockFileReader.readFile(Mockito.anyString())).thenThrow(new IOException());

    // Тестирование
    FileProcessor fileProcessor = new FileProcessor(mockFileReader);
    String result = fileProcessor.processFile("test.txt");

    // Проверка
    assertEquals("Error", result);
}

В этом тесте мы гарантируем, что когда метод readFile вызывается, IOException выбрасывается, и FileProcessor корректно обрабатывает это исключение.

Mockito также позволяет мокировать исключения для конкретных параметров вызываемого метода. Это полезно, когда поведение метода зависит от предоставленных аргументов.

@Test
public void whenSpecificArgument_thenThrowException() throws IOException {
    // Создание мока
    FileReader mockFileReader = Mockito.mock(FileReader.class);

    // Мокирование исключения для конкретного аргумента
    Mockito.when(mockFileReader.readFile("specific.txt")).thenThrow(new IOException());

    // Тестирование
    FileProcessor fileProcessor = new FileProcessor(mockFileReader);
    String result = fileProcessor.processFile("specific.txt");

    // Проверка
    assertEquals("Error", result);
}

Здесь мы гарантируем, что IOException будет выброшен только тогда, когда readFile вызывается с "specific.txt" в качестве аргумента.

ArgumentMatchers в Mockito

ArgumentMatchers - это специальные условия, которые мы можем применять к аргументам вызываемых методов. Они позволяют определить правила для любых аргументов или их определенных комбинаций.

Интерфейс UserService, который мы хотим протестировать:

public interface UserService {
    User findUser(String username, int age);
}

Теперь, мы хотим замокировать этот сервис и определить поведение метода findUser.

Допустим, нам не важно, с какими аргументами вызывается метод - мы хотим вернуть определенного пользователя для любых значений.

@Test
public void whenUsingAnyMatcher_thenUserIsReturned() {
    UserService mockUserService = Mockito.mock(UserService.class);
    User expectedUser = new User("Alice", 30);

    // Стаббинг с использованием any()
    Mockito.when(mockUserService.findUser(Mockito.anyString(), Mockito.anyInt())).thenReturn(expectedUser);

    // Вызов метода с любыми аргументами
    User result = mockUserService.findUser("Bob", 25);

    // Проверка
    assertEquals(expectedUser, result);
}

Вне зависимости от передаваемых в findUser аргументов, всегда будет возвращаться expectedUser.

Теперь, если нам нужно определить поведение метода для конкретных аргументов:

@Test
public void whenUsingEqMatcher_thenSpecificUserIsReturned() {
    UserService mockUserService = Mockito.mock(UserService.class);
    User specificUser = new User("Bob", 25);

    // Стаббинг: метод должен быть вызван с конкретными аргументами
    Mockito.when(mockUserService.findUser(Mockito.eq("Bob"), Mockito.eq(25))).thenReturn(specificUser);

    // Вызов метода с конкретными аргументами
    User result = mockUserService.findUser("Bob", 25);

    // Проверка
    assertEquals(specificUser, result);
}

Здесь findUser возвращает specificUser только если вызывается с аргументами "Bob" и 25.

ArgumentMatchers также полезны при верификации вызовов методов:

@Test
public void verifyWithArgumentMatchers() {
    // Создание мока
    UserService mockUserService = Mockito.mock(UserService.class);

    // Вызов метода
    mockUserService.findUser("Alice", 30);

    // Верификация: был ли метод вызван с определенными аргументами
    Mockito.verify(mockUserService).findUser(Mockito.eq("Alice"), Mockito.eq(30));
}

Спай-объекты (Spying) в Mockito

Спай-объекты - это специальные моки, которые в основном ведут себя как обычные объекты, но позволяют переопределять поведение отдельных методов. Они мегаполезны, когда вам нужно тестировать реальный объект, но при этом изменить поведение некоторых его частей.

Допустим, у нас есть класс ListManager, который мы хотим протестировать:

public class ListManager {
    public List<String> createList() {
        return new ArrayList<>();
    }

    public int getListSize(List<String> list) {
        return list.size();
    }
}

Мы можем создать спай для этого класса следующим образом:

@Test
public void spyExample() {
    ListManager spyListManager = Mockito.spy(new ListManager());

    // Использование реального метода
    List<String> list = spyListManager.createList();
    list.add("Item");

    // Переопределение поведения метода getListSize
    Mockito.when(spyListManager.getListSize(list)).thenReturn(100);

    // Тестирование
    int size = spyListManager.getListSize(list);

    // Проверка
    assertEquals(100, size);
}

Мы создали спай ListManager, который использует реальный метод createList. Однако, мы переопределили поведение метода getListSize, чтобы он возвращал 100, независимо от реального размера списка.

Также как и с моками, мы можем использовать верификацию с спай-объектами:

@Test
public void verifySpy() {
    // Создание спай-объекта
    ListManager spyListManager = Mockito.spy(new ListManager());

    // Вызов метода
    List<String> list = spyListManager.createList();
    list.add("Item");
    spyListManager.getListSize(list);

    // Верификация вызова метода
    Mockito.verify(spyListManager).getListSize(list);
}

Здесь мы проверяем, что метод getListSize был действительно вызван на спай-объекте.

Тестирование с использованием аннотаций

Аннотация @Mock используется для создания мок-объектов. Вместо явного вызова Mockito.mock(Class), вы можете аннотировать поля вашего тестового класса.

Рассмотрим очередной пример с интерфейсом DataService:

public interface DataService {
    int retrieveData();
}

Мы можем создать мок DataService с использованием @Mock:

public class DataServiceTest {

    @Mock
    DataService mockDataService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testRetrieveData() {
        Mockito.when(mockDataService.retrieveData()).thenReturn(5);
        assertEquals(5, mockDataService.retrieveData());
    }
}

@InjectMocks автоматически вставляет мок-объекты в тестируемый объект. Это удобно, когда у вас есть сложные зависимости.

Предположим, у нас есть класс DataProcessor, который зависит от DataService:

public class DataProcessor {
    private DataService dataService;

    public DataProcessor(DataService dataService) {
        this.dataService = dataService;
    }

    public int process() {
        return dataService.retrieveData() * 2;
    }
}

Мы можем использовать @InjectMocks для автоматического внедрения мока DataService в DataProcessor:

public class DataProcessorTest {

    @Mock
    DataService mockDataService;

    @InjectMocks
    DataProcessor dataProcessor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testProcess() {
        Mockito.when(mockDataService.retrieveData()).thenReturn(5);
        assertEquals(10, dataProcessor.process());
    }
}

@Spy позволяет создать спай-объекты, которые частично мокируют реальные объекты.

Допустим, у нас есть класс ListManager:

public class ListManager {
    public int getListSize(List<String> list) {
        return list.size();
    }
}

Мы можем использовать @Spy для тестирования этого класса:

public class ListManagerTest {

    @Spy
    ListManager spyListManager;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testGetListSize() {
        List<String> list = new ArrayList<>(Arrays.asList("item1", "item2"));
        Mockito.doReturn(100).when(spyListManager).getListSize(list);
        assertEquals(100, spyListManager.getListSize(list));
    }
}

@RunWith(MockitoJUnitRunner.class)

@RunWith(MockitoJUnitRunner.class) - это аннотация JUnit, которая указывает, что тесты в классе должны запускаться с помощью специального "раннера" - MockitoJUnitRunner. Этот раннер инициализирует поля, аннотированные как @Mock, @Spy, @InjectMocks, автоматически, без необходимости вызывать MockitoAnnotations.initMocks(this) в каждом тестовом классе.

Допустим, у нас есть следующие классы:

public interface DataService {
    int retrieveData();
}

public class DataProcessor {
    private DataService dataService;

    public DataProcessor(DataService dataService) {
        this.dataService = dataService;
    }

    public int process() {
        return dataService.retrieveData() * 2;
    }
}

Наша цель - протестировать DataProcessor, используя мок DataService.

Для этого мы используем аннотацию @RunWith(MockitoJUnitRunner.class):

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class DataProcessorTest {

    @Mock
    DataService mockDataService;

    @Test
    public void testProcess() {
        when(mockDataService.retrieveData()).thenReturn(5);

        DataProcessor dataProcessor = new DataProcessor(mockDataService);

        assertEquals(10, dataProcessor.process());
    }
}

Здесь MockitoJUnitRunner автоматически инициализирует mockDataService. Вам не нужно вызывать MockitoAnnotations.initMocks(this) в методе @Before.

Немного про Mockito и Spring Boot

В Spring Boot, Mockito используется для создания моков или спай-объектов для Spring Beans. Это позволяет имитировать поведение этих бинов, таким образом, чтобы тестировать взаимодействие различных слоев приложения (например, контроллеров, сервисов и репозиториев) без необходимости взаимодействовать с реальными внешними системами, такими как базы данных.

Допустим, у нас есть следующий сервис в приложении Spring Boot:

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

И соответствующий репозиторий:

public interface UserRepository extends JpaRepository<User, Long> {
    // Базовые методы JpaRepository
}

Нам нужно протестировать метод getUserById в UserService, не обращаясь к реальной бд.

Используем Spring Boot Test и Mockito для создания моков:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.boot.test.context.SpringBootTest;

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

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void whenGetUserById_thenUserReturned() {
        Long userId = 1L;
        User mockUser = new User(userId, "Test User");
        when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

        User result = userService.getUserById(userId);

        assertNotNull(result);
        assertEquals(mockUser, result);
    }
}

В этом тесте:

  • @ExtendWith(SpringExtension.class): Интегрирует тест с Spring TestContext Framework.

  • @SpringBootTest: Говорит Spring Boot загрузить контекст приложения, чтобы тесты могли быть интеграционными.

  • @Mock: Создает мок-объект для UserRepository.

  • @InjectMocks: Автоматически вставляет мок userRepository в userService.


Mockito - это не просто инструмент для мокирования, он повышает качества кода и упрощает тестирование.

Не за горой новогодние праздники, в связи с этим хочу порекомендовать вам бесплатный урок, где вы создадите интерактивную Новогоднюю открытку - графическое приложение с использованием Java и LibGDX, с нуля. В ходе вебинара вы сможете предлагать идеи, которые хотелось бы реализовать в проекте.

Комментарии (2)


  1. aleksandy
    14.12.2023 06:54

    с помощью аннотации @RunWith(MockitoJUnitRunner.class) в JUnit 4

    Лучше для этого использовать @Rule


  1. RyAtex
    14.12.2023 06:54

    Очень полезная статья, я бы сказал, единственная, по Unit-тестам, написанная понятным языком, спасибо, сохранил себе. Сейчас активно изучаю Java, как раз потихоньку копаю тестирование кода