Тестирование #
1. На чем пишутся тесты? #
- Unit-тесты: В основном используются фреймворки JUnit для написания модульных тестов в Java.
- Интеграционные тесты: Используют Spring Test, Testcontainers для интеграционного тестирования с базами данных и внешними системами.
- Mocking: Фреймворки, такие как Mockito применяются для создания заглушек (mock-объектов) в unit-тестах.
- UI-тесты: Для тестирования интерфейсов часто используют Selenium.
- Performance тесты: Инструменты вроде JMeter.
2. Mock vs spy #
Критерий | Mock | Spy |
---|---|---|
Описание | Полностью имитирует поведение объекта. Вы задаёте ожидаемые ответы на вызовы методов. | Реальный объект, но с возможностью “шпионить” за вызовами методов. |
Подход | Заменяет объект, тестируется только взаимодействие (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:
- Подмена методов: Мы используем
when(spyObject.method()).thenReturn(value)
, чтобы переопределить поведение метода для тестирования. - Реальное поведение: Все остальные методы работают как у настоящего объекта. Например, если мы добавили элемент в список через
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
.