Тестирование #
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
.
9. Основные аннотации в тестировании #
✅ Основные аннотации JUnit 5:
Аннотация | Назначение |
---|---|
@Test | Обозначает метод как тестовый. |
@BeforeEach | Метод выполняется перед каждым тестом. |
@AfterEach | Метод выполняется после каждого теста. |
@BeforeAll | Выполняется один раз перед всеми тестами (должен быть static ). |
@AfterAll | Выполняется один раз после всех тестов (должен быть static ). |
@DisplayName | Назначает удобное имя тесту, отображается в отчётах. |
@Disabled | Отключает тест (временно пропускает). |
@Nested | Позволяет группировать тесты во вложенные классы. |
@Tag | Помечает тест определённым тегом (удобно для фильтрации). |
@ParameterizedTest | Используется для параметризованных тестов (с разными входами). |
🔹 Простой пример:
import org.junit.jupiter.api.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserServiceTest {
@BeforeAll
static void initAll() {
System.out.println("⚙️ Before all tests");
}
@BeforeEach
void init() {
System.out.println("🧪 Before each test");
}
@Test
@DisplayName("Проверка регистрации пользователя")
void testUserRegistration() {
Assertions.assertEquals(2, 1 + 1);
}
@Test
@Disabled("Временно отключен")
void disabledTest() {
Assertions.fail("Этот тест не будет выполнен");
}
@AfterEach
void tearDown() {
System.out.println("✅ After each test");
}
@AfterAll
static void tearDownAll() {
System.out.println("🏁 After all tests");
}
}
🔹 Параметризованный тест:
@ParameterizedTest
@ValueSource(strings = {"admin", "user", "guest"})
void testRoles(String role) {
Assertions.assertTrue(role.length() > 0);
}