Тестирование

Тестирование #

1. На чем пишутся тесты? #

  • Unit-тесты: В основном используются фреймворки JUnit для написания модульных тестов в Java.
  • Интеграционные тесты: Используют Spring Test, Testcontainers для интеграционного тестирования с базами данных и внешними системами.
  • Mocking: Фреймворки, такие как Mockito применяются для создания заглушек (mock-объектов) в unit-тестах.
  • UI-тесты: Для тестирования интерфейсов часто используют Selenium.
  • Performance тесты: Инструменты вроде JMeter.

2. Mock vs spy #

КритерийMockSpy
ОписаниеПолностью имитирует поведение объекта. Вы задаёте ожидаемые ответы на вызовы методов.Реальный объект, но с возможностью “шпионить” за вызовами методов.
ПодходЗаменяет объект, тестируется только взаимодействие (behavior verification).Используется реальный объект, но вы можете подменять результаты вызовов его методов.
Когда использоватьКогда необходимо полностью изолировать тестируемый объект от зависимости.Когда хотите протестировать реальный объект, но также отследить вызовы его методов.
Пример использованияПроверка взаимодействия с внешней зависимостью, например, базой данных.Проверка поведения конкретного метода объекта, оставаясь близко к реальной логике.
Типы подменВы задаёте возвращаемые значения для всех методов объекта.Реальные методы объекта выполняются, если не настроена их подмена.

3. Параметризованные тесты #

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

@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testWithParameters(int number) {
    assertTrue(number > 0);
}

Типы параметризованных тестов в JUnit 5:

АннотацияОписание
@ValueSourceПередаёт фиксированные наборы данных (массив значений).
@CsvSourceПередаёт данные в формате CSV.
@CsvFileSourceЗагружает данные из файла CSV.
@MethodSourceИспользует метод для предоставления набора данных.
@EnumSourceПередаёт значения перечислений (enums).

4. Unit тесты. Что это и зачем? #

Unit-тестирование – это тестирование отдельных компонентов (методов, классов, модулей) в изоляции от остальной системы.

📌 Цель Unit-тестов:

  • Проверить корректность работы каждого модуля.
  • Локализовать ошибки на уровне методов и классов.
  • Упростить рефакторинг кода (помогает не сломать старую логику при изменениях).

📌 Пример Unit-теста (JUnit 5 + Mockito)

class Calculator {
    int add(int a, int b) {
        return a + b;
    }
}

class CalculatorTest {
    @Test
    void testAddition() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));
    }
}

Проверяем только метод add() без зависимостей.


5. Unit тесты vs интеграционные тесты #

КритерийUnit-тестыИнтеграционные тесты
Что тестируют?Отдельные методы, классыВзаимодействие компонентов
Зависимости?Заменяются mock-объектамиИспользуются реальные сервисы (БД, API)
Скорость выполнения🟢 Быстро (миллисекунды)🔴 Медленно (секунды и больше)
ПримерCalculator.add(2, 3)UserService работает с Database

📌 Unit-тесты нужны для проверки отдельных компонентов, а интеграционные – для тестирования их совместной работы.


6. Mock. Как работает? #

Mock – это объект, который полностью подменяет реальный объект и позволяет вам контролировать его поведение для тестов. Mock не выполняет фактическую логику, а просто возвращает заранее определенные значения, которые вы укажете в тестах.

📌 Основное назначение Mock:

  • Для того, чтобы тестировать код в изоляции, не задействуя реальные объекты или зависимости, например, БД или внешние API.
  • Вы можете определить, что вернет метод (например, return 42), но не заботиться о том, как он это делает.

Пример использования Mock (Mockito)

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

public class UserService {
    private UserRepository repository;

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

    public String getUserName(int id) {
        return repository.findNameById(id);  // Ищем имя пользователя по id
    }
}

@Test
public void testMock() {
    // Создаем mock-объект
    UserRepository mockRepo = mock(UserRepository.class);
    
    // Указываем поведение mock-объекта
    when(mockRepo.findNameById(1)).thenReturn("Alice");

    // Используем mock-объект в сервисе
    UserService service = new UserService(mockRepo);
    
    // Проверяем результат
    assertEquals("Alice", service.getUserName(1));
}

✔ В данном примере мы подменяем UserRepository с помощью mock-объекта и говорим ему, что он должен возвращать "Alice", когда вызывается метод findNameById(1).


7. Spy. Как работает? #

Spy – это объект, который ведет себя как настоящий объект, но позволяет подменить поведение некоторых методов или проверить, были ли вызваны методы.

📌 Основное назначение Spy:

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

Как работает Spy?

  • Spy сохраняет реальные данные и действия объекта, но при этом позволяет переопределять отдельные методы.
  • В отличие от Mock, где вы полностью контролируете поведение, Spy позволяет тестировать и фактическую логику, и при этом подменять метод, если это необходимо.

Пример использования Spy (Mockito)

Предположим, у нас есть объект, который взаимодействует с реальной коллекцией, и нам нужно изменить только один метод, чтобы протестировать его.

@Test
public void testSpy() {
    // Создаем реальный объект
    List<String> list = new ArrayList<>();
    
    // Создаем spy-объект для этого списка
    List<String> spyList = spy(list);
    
    // Добавляем элемент в реальный список
    spyList.add("Hello");

    // Переопределяем метод size(), чтобы он всегда возвращал 100
    when(spyList.size()).thenReturn(100);

    // Проверяем результат
    assertEquals(100, spyList.size());  // Мы подменили размер
    assertEquals(1, spyList.size());    // Но добавление элемента осталось реальным
}

Основные моменты по работе Spy:

  1. Подмена методов: Мы используем when(spyObject.method()).thenReturn(value), чтобы переопределить поведение метода для тестирования.
  2. Реальное поведение: Все остальные методы работают как у настоящего объекта. Например, если мы добавили элемент в список через spyList.add(), то этот элемент действительно добавится в spyList, и метод size() вернет реальный результат, если мы не подменили его.

8. Каким образом проверяются результаты тестов? #

✅ 1. Проверка значений (Assertions)

📌 Используется assertEquals, assertTrue, assertFalse и др.

assertEquals(5, calculator.add(2, 3)); // Проверка, что результат 5
assertTrue(user.isActive()); // Проверка, что юзер активен

✅ 2. Проверка вызовов Mock (Mockito)

📌 Проверяем, вызывался ли метод и сколько раз.

verify(mockRepo, times(1)).findNameById(1);

✔ Убедимся, что метод findNameById(1) был вызван ровно 1 раз.

✅ 3. Проверка исключений

📌 Проверяем, выбросил ли метод нужное исключение.

assertThrows(IllegalArgumentException.class, () -> service.findUser(-1));

✔ Проверяем, что при передаче -1 выбрасывается IllegalArgumentException.