Введение в Java JUnit
Что такое JUnit, как добавить его в проект Java и как написать первый тест с JUnit.
JUnit — это стандартный фреймворк для написания автоматических тестов в Java. Тест — это небольшой метод, который запускает часть вашего кода и утверждает, что он работает ожидаемым образом; задача JUnit — найти эти методы, запустить каждый в изоляции, проверить утверждения и сообщить, какие прошли, а какие нет. Текущее поколение, JUnit 5 (также называемое JUnit Jupiter), поставляется в виде набора небольших библиотек, которые добавляются в сборку — оно не является частью JDK — и управляет шагом mvn test / gradle test, который является обязательным условием почти в каждом CI Java-проекта.
Зачем нужен фреймворк тестирования
Можно проверять код вручную с помощью методов main и println — но это плохо масштабируется. Фреймворк даёт вам четыре вещи, которые иначе пришлось бы реализовывать самому:
- Обнаружение — он автоматически находит каждый метод
@Test; вам не нужно поддерживать список вручную. - Изоляция — каждый тест получает свежую фикстуру, поэтому один тест не может нарушить другой.
- Утверждения — богатый словарь (
assertEquals,assertThrows, …), который выдаёт точные сообщения об ошибках. - Отчётность — единый итог прохождения/провала, понятный инструменту сборки и IDE.
Сделайте это правильно один раз, и тесты станут дёшевы в написании — а это и есть суть: дешёвые тесты пишутся, и код, покрытый тестами, можно менять без страха.
Добавление JUnit в проект
JUnit 5 — это зависимость, объявленная в файле сборки. В Maven агрегатор junit-jupiter подтягивает API (для компиляции) и движок (для запуска):
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>В Gradle это две строки плюс переключатель useJUnitPlatform():
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
useJUnitPlatform()
}Исходные коды тестов располагаются в src/test/java, зеркально повторяя пакет тестируемого класса. Область test гарантирует, что JUnit не попадёт в производственный артефакт.
Ваш первый тест
Тест JUnit — это обычный метод, аннотированный @Test. Внутри него вы вызываете тестируемый код и проверяете результат. Вот Calculator и класс тестов для него:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CalculatorTest {
private final Calculator calc = new Calculator();
@Test
void addReturnsSum() {
assertEquals(5, calc.add(2, 3));
}
@Test
void divideByZeroThrows() {
assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
}
}Обратите внимание на паттерн, которому следует каждый тест: Arrange — подготовьте фикстуру, Act — вызовите метод, Assert — проверьте результат. Тестовые методы имеют package-private доступ (в JUnit 5 public не нужен) и возвращают void. Запустите mvn test, и сборка станет зелёной только при выполнении всех утверждений.
Аннотации и утверждения, которые используются чаще всего
Поверхность JUnit небольшая. Несколько членов покрывают подавляющее большинство реальных тестов:
| Элемент | Пакет / класс | Назначение |
|---|---|---|
@Test | org.junit.jupiter.api | Помечает метод как тест |
@BeforeEach / @AfterEach | org.junit.jupiter.api | Выполняется до/после каждого теста (настройка / сброс фикстур) |
@BeforeAll / @AfterAll | org.junit.jupiter.api | Выполняется один раз до/после всех тестов в классе |
@DisplayName | org.junit.jupiter.api | Человекочитаемое имя для отчётов |
@Disabled | org.junit.jupiter.api | Временно пропустить тест |
assertEquals(exp, act) | Assertions | Провал, если два значения не равны |
assertTrue / assertFalse | Assertions | Провал, если boolean не выполняется |
assertThrows(type, exec) | Assertions | Провал, если лямбда не бросает то исключение |
assertNull / assertNotNull | Assertions | Провал при неверном значении null |
@BeforeEach обеспечивает каждому тесту чистое состояние — JUnit создаёт новый экземпляр тестового класса для каждого @Test, а затем выполняет настройку, поэтому состояние никогда не утекает между тестами.
Что JUnit делает за вас, в одном запускаемом файле
В коде ниже нет JUnit в classpath (это внешняя библиотека, не часть JDK), поэтому пример переимплементирует основной цикл JUnit на чистом Java: фикстура пересоздаётся перед каждым тестом, небольшой набор вспомогательных assertXxx, список тестовых методов, выполняемых независимо, и итоговый счётчик прошедших/проваленных. Именно это и автоматизирует JUnit — видя это без обёрток, вы сразу понимаете реальный фреймворк. Один тест намеренно проваливается, чтобы вы увидели, как выглядит «красный» результат.
Что следует вынести из запуска:
- Три правильных теста печатают
PASS, а четвёртый печатаетFAIL deliberatelyFailing -> expected <10> but was <5>— точное сообщение об ошибке, а не просто «тест не прошёл». Эта разница (expected … but was …) — именно то, что даётassertEqualsв JUnit, и именно это позволяет диагностировать красный тест с первого взгляда. setUp()выполняется перед каждым тестом, поэтомуcalc— это свежийCalculatorкаждый раз. Это контракт@BeforeEach: тесты изолированы, и порядок их выполнения никогда не может иметь значения, потому что ни один из них не разделяет изменяемое состояние.divideThrowsOnZeroпроходит, утверждая, что исключение было брошено —assertThrowsпревращает «здесь должна быть ошибка» в первоклассное, позитивное утверждение вместо хрупкого try/catch. Ожидаемые исключения — это поведение, достойное тестирования, а не ошибки, которые нужно замалчивать.- Итоговый подсчёт —
Tests run: 4, Passed: 3, Failed: 1, Assertions: 5— это отчёт. Один провалившийся тест из четырёх всё равно переключает всю сборку вRED; CI рассматривает любой сбой как остановку, именно поэтому зелёный набор тестов имеет смысл. - Здесь ничего не импортировало JUnit, однако форма идентична: обнаружение методов-аннотаций, настройка перед каждым тестом, утверждения, итог. Ценность JUnit в том, что он автоматизирует этот цикл (и добавляет обнаружение, параллелизм, параметризованные тесты и интеграцию с IDE), так что вы пишете только тела тестов.
Что охватывает остальная часть этого раздела
Этот раздел строится на основе цикла, который вы только что увидели:
- Аннотации JUnit —
@Test,@DisplayName,@Disabledи остальные маркеры. - Жизненный цикл теста — как
@BeforeEach/@AfterEach/@BeforeAll/@AfterAllдают каждому тесту чистую фикстуру. - Утверждения — полный каталог
assertXxx, включаяassertThrowsдля тестирования исключений. - Параметризованные тесты — запуск одного тела теста с множеством входных данных вместо копирования случаев.
- Мокирование с Mockito — замена зависимостей класса заглушками, чтобы юнит-тест оставался юнит-тестом.
Если перед погружением в API вы хотите понять, зачем вообще нужно автоматическое тестирование, см. Тестирование в Java. В противном случае следующая глава начинается там, где начинается каждый набор тестов: определение тестового класса и его запуск.