Введение в тестирование Java
Зачем нужно тестирование в Java, пирамида тестирования и обзор популярных фреймворков для тестирования.
Автоматическое тестирование — это способ доказать, что код делает именно то, что вы ожидаете, и продолжать это доказывать по мере изменений кода. Вместо того чтобы запускать программу вручную и проверять вывод на глаз, вы пишете небольшие программы, которые выполняют ваш код и автоматически проверяют результаты. В Java эта экосистема строится вокруг фреймворков JUnit и Mockito, но основные идеи — расставить, выполнить, проверить — достаточно просты, чтобы реализовать их вручную. Эта глава описывает общую картину, прежде чем последующие главы подробно рассмотрят каждый инструмент.
Зачем нужно автоматическое тестирование
Тест — это небольшая, повторяемая проверка того, что фрагмент кода работает корректно. Ценность заключается не в первом запуске, а в каждом последующем. Как только поведение зафиксировано в тесте, любое изменение, которое его нарушает, немедленно и громко сигнализирует об ошибке — вместо того чтобы всплыть в виде бага в продакшене через несколько недель. Тесты также документируют намерения: хорошо названный тест говорит, что код должен делать.
// A test names a behavior, runs the code, and asserts the outcome.
@Test
void addsTwoPositiveNumbers() {
int result = Calculator.add(2, 3);
assertEquals(5, result); // fails the build if result != 5
}Цель — быстрая обратная связь. Зелёный набор тестов означает, что вы можете рефакторить с уверенностью; красный сразу указывает на то, что сломалось.
Пирамида тестирования
Тесты делятся на уровни, которые обычно изображают в виде пирамиды. Модульные тесты находятся в основании: их много, они быстрые, каждый проверяет один класс или метод в изоляции. Интеграционные тесты находятся в середине: их меньше, они медленнее, проверяют взаимодействие компонентов (ваш код вместе с базой данных, например). Сквозные (E2E) тесты находятся на вершине: их мало, они самые медленные, управляют всем приложением так, как это делал бы пользователь.
| Уровень | Область | Скорость | Количество | Инструменты Java |
|---|---|---|---|---|
| Модульный | один класс/метод | быстрая (мс) | много | JUnit, AssertJ |
| Интеграционный | несколько компонентов | средняя | некоторые | JUnit, Testcontainers |
| Сквозной | вся система | медленная | мало | Selenium, REST-assured |
Форма имеет значение: опирайтесь на дешёвые и быстрые модульные тесты для большей части покрытия, а медленные и хрупкие E2E-тесты оставляйте для небольшого числа критически важных пользовательских сценариев.
Паттерн «расставить–выполнить–проверить»
Практически каждый тест в любом фреймворке следует одной и той же трёхшаговой структуре. Расставьте входные данные и зависимости. Выполните код, который тестируется. Проверьте, что результат соответствует ожидаемому. Визуальное разделение этих шагов делает тест удобным для чтения и отладки при сбое.
@Test
void rejectsBlankUsername() {
// Arrange
UserService service = new UserService();
// Act
boolean valid = service.isValidUsername(" ");
// Assert
assertFalse(valid);
}Неудачная проверка выбрасывает исключение, фреймворк фиксирует его, и выполнение продолжается на следующем тесте — таким образом, одно сломанное поведение никогда не скрывает остальные.
JUnit — стандартный раннер
JUnit — де-факто фреймворк для модульного тестирования в Java. Вы аннотируете методы с помощью @Test, JUnit обнаруживает их через рефлексию, запускает каждый и сообщает об успехе или неудаче. Проверки вроде assertEquals, assertTrue и assertThrows — это статические вспомогательные методы, которые провалят тест, если ожидание не выполнено. В реальных проектах JUnit запускается через инструмент сборки (плагин Surefire для Maven или задача test в Gradle), а не вручную.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void dividesNumbers() {
assertEquals(4, Calculator.divide(8, 2));
}
@Test
void throwsOnDivideByZero() {
assertThrows(ArithmeticException.class, () -> Calculator.divide(1, 0));
}
}Поскольку JAR-файл JUnit и инструменты сборки недоступны в этой среде, пример ниже реализует ту же идею с нуля — минималистичный обвязчик, который запускает именованные проверки и подсчитывает успехи и неудачи, — именно то, что @Test вместе с assertEquals делают под капотом.
Что стоит вынести из запуска:
- Каждый вызов
assertEquals— это один тест-кейс: расставляем входные данные, выполняемaddилиisBlank, проверяем результат — в точности то, что делает метод@Testв JUnit. - Успешная проверка выводит
PASS, а неудачная —FAILс ожидаемым и фактическим значениями, что является диагностикой, которую дают вам сообщения проверок JUnit. - Намеренно неверный случай (
expected 10 but got 5) показывает, как выглядит красный тест: обвязчик продолжает выполнять оставшиеся проверки вместо того, чтобы остановиться на первой ошибке. - Итоговый отчёт показывает 5 всего, 4 успешно, 1 неудача — тот же отчёт об успехах и неудачах, который выводит раннер тестов в конце выполнения.
- Поскольку один тест провалился, программа завершается с
BUILD FAILURE, демонстрируя, почему один сломанный тест должен ломать весь билд в CI.
Как всё это соединяется
Инструменты тестирования Java выстраиваются в слои друг над другом — от простых проверок до полной интеграции со сборкой:
- Проверки (
assertEquals,assertThrows) формулируют, что должно быть истинным. - JUnit обнаруживает и запускает методы
@Testи сообщает о результатах. - Mockito предоставляет поддельные зависимости, чтобы можно было тестировать модуль в изоляции.
- Maven или Gradle встраивает набор тестов в процесс сборки, проваливая её при любом красном тесте.
- CI запускает сборку при каждом коммите, чтобы сломанный код никогда не попал в основную ветку.
Каждая последующая глава посвящена одной ступени этой лестницы — сначала аннотации и проверки JUnit, затем имитация с Mockito, затем интеграция тестов в Maven и Gradle. Понимание места каждого инструмента помогает сохранить целостность всей картины тестирования.