JUnit — это фреймворк для автоматических тестов на Java: вы пишете класс с методами, помеченными аннотацией @Test, внутри проверяете результат методами-утверждениями (assertions), а JUnit находит такие методы, запускает их и сообщает, какие прошли, а какие упали. Актуальная ветка на сентябрь 2026 — JUnit 6 (версия 6.0.0 вышла 30 сентября 2025 года, свежий релиз — 6.1.3 от августа 2026), ей нужна Java 17 и новее. JUnit 5 (последняя линия 5.14.x) остается рабочим вариантом для проектов на Java 8-16, а JUnit 4 — legacy.
Содержание
Ниже — один полный пример теста, таблица аннотаций, основные assertions, подключение в Maven и Gradle и разбор типовых ошибок. Все примеры проверены 24.09.2026 на JUnit 6.1.3 и Temurin JDK 25.
JUnit 4, 5 и 6: что сейчас выбирать
Сначала три термина, которые путают чаще всего. Начиная с JUnit 5 это не одна библиотека, а три части:
- JUnit Platform — «запускалка»: находит тесты и передает их движкам, с ней работают Maven, Gradle и IDE;
- JUnit Jupiter — API для написания тестов (
@Test,Assertions) и движок, который их выполняет; - JUnit Vintage — движок, который умеет запускать старые тесты JUnit 3/4 на новой платформе.
В JUnit 6 у всех частей единый номер версии (раньше платформа имела номер 1.x при Jupiter 5.x), а движок Vintage помечен устаревшим (deprecated since 6.0): его стоит использовать только на время миграции.
| JUnit 4 | JUnit 5 | JUnit 6 | |
|---|---|---|---|
| Минимальная Java | 5 | 8 | 17 |
| Пакет аннотаций | org.junit |
org.junit.jupiter.api |
org.junit.jupiter.api |
| Перед каждым тестом | @Before |
@BeforeEach |
@BeforeEach |
| Параметризованные тесты | раннер Parameterized |
@ParameterizedTest |
@ParameterizedTest |
| Статус в 2026 | legacy, только поддержка | для проектов на Java 8-16 | основная ветка |
Практическое правило: новый проект на Java 17+ — сразу JUnit 6. Проект на Java 11 — JUnit 5.14.x, код тестов при переходе на 6 почти не меняется: аннотации и assertions те же, меняются версия зависимости и требования к Java.
Минимальный полный пример
Проверяем метод, который считает цену со скидкой. Деньги храним целым числом в копейках, чтобы не ловить ошибки округления double.
public class PriceCalculator {
// Цена со скидкой в копейках; цена >= 0, скидка - целые проценты от 0 до 100
public long applyDiscount(long priceKopecks, int percent) {
if (priceKopecks < 0) {
throw new IllegalArgumentException("price must be >= 0, got " + priceKopecks);
}
if (percent < 0 || percent > 100) {
throw new IllegalArgumentException("percent must be 0..100, got " + percent);
}
// multiplyExact бросит ArithmeticException вместо тихого переполнения long
return priceKopecks - Math.multiplyExact(priceKopecks, percent) / 100;
}
}
Проверки входа здесь не для красоты: без них applyDiscount(-999, 50) вернул бы -500, а на очень большой цене умножение молча переполнило бы long и дало бы мусор. Math.multiplyExact в таком случае бросает ArithmeticException: long overflow.
Тестовый класс для него:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class PriceCalculatorTest {
private PriceCalculator calc;
@BeforeEach
void setUp() {
calc = new PriceCalculator(); // новый объект перед каждым тестом
}
@Test
@DisplayName("Скидка 10% от 1000 рублей")
void tenPercent() {
assertEquals(90_000, calc.applyDiscount(100_000, 10));
}
@Test
void edgeCases() {
assertAll(
() -> assertEquals(100_000, calc.applyDiscount(100_000, 0)),
() -> assertEquals(0, calc.applyDiscount(100_000, 100))
);
}
@Test
void rejectsWrongPercent() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> calc.applyDiscount(100_000, 150));
assertEquals("percent must be 0..100, got 150", e.getMessage());
}
@ParameterizedTest(name = "{0} коп. - {1}% = {2} коп.")
@CsvSource({
"100000, 15, 85000",
"999, 50, 500",
"1, 99, 1"
})
void manyPrices(long price, int percent, long expected) {
assertEquals(expected, calc.applyDiscount(price, percent));
}
}
Запуск через консольный лаунчер JUnit (дерево результатов, фрагмент):
├─ JUnit Jupiter ✔
│ └─ PriceCalculatorTest ✔
│ ├─ edgeCases() ✔
│ ├─ Скидка 10% от 1000 рублей ✔
│ ├─ rejectsWrongPercent() ✔
│ └─ manyPrices(long, int, long) ✔
│ ├─ "100000" коп. - "15"% = "85000" коп. ✔
│ ├─ "999" коп. - "50"% = "500" коп. ✔
│ └─ "1" коп. - "99"% = "1" коп. ✔
...
[ 6 tests successful ]
[ 0 tests failed ]
Что здесь происходит:
- тестовый класс и методы не обязаны быть
public— в Jupiter хватает видимости пакета, но метод не должен бытьprivateилиstatic; @BeforeEachвыполняется перед каждым тестом, а JUnit по умолчанию создает новый экземпляр класса на каждый метод, поэтому тесты не делят состояние;@ParameterizedTestпревратил один метод в три запуска, по одному на строку@CsvSource; строки из CSV JUnit сам привел кlongиint;- кавычки вокруг аргументов в имени — поведение JUnit 6 для текстовых аргументов (в 5.14 имя выводится без кавычек); отключается атрибутом
quoteTextArguments = false, которого в JUnit 5 нет.
Аннотации JUnit: шпаргалка
| Аннотация | Что делает | Аналог в JUnit 4 |
|---|---|---|
@Test |
помечает тестовый метод | @Test (другой пакет) |
@BeforeEach / @AfterEach |
код до и после каждого теста | @Before / @After |
@BeforeAll / @AfterAll |
один раз на класс, метод static |
@BeforeClass / @AfterClass |
@DisplayName |
читаемое имя теста в отчете | нет |
@Disabled |
временно отключить тест | @Ignore |
@Tag("slow") |
метка для фильтра запуска | @Category |
@Nested |
вложенный класс-группа тестов | нет |
@ParameterizedTest + источник |
один метод на много наборов данных | раннер Parameterized |
@RepeatedTest(5) |
повторить тест N раз | нет |
@Timeout(2) |
тест падает, если идет дольше 2 секунд | @Test(timeout = ...) |
@TempDir |
временный каталог, который JUnit удалит после теста | правило TemporaryFolder |
Источники данных для @ParameterizedTest: @ValueSource (один аргумент из списка), @CsvSource (несколько аргументов строкой), @EnumSource (значения перечисления), @MethodSource (метод, который возвращает поток аргументов — для объектов, которые не записать строкой).
Assertions: чем проверять результат
Все проверки — статические методы класса org.junit.jupiter.api.Assertions, обычно их подключают через import static.
| Метод | Когда использовать |
|---|---|
assertEquals(expected, actual) |
сравнить значение с ожидаемым |
assertEquals(0.3, x, 1e-9) |
сравнить double с допуском |
assertTrue / assertFalse |
проверить условие |
assertNull / assertNotNull |
проверить ссылку |
assertThrows(Тип.class, () -> ...) |
код должен бросить исключение; метод возвращает его для проверки сообщения |
assertAll(...) |
выполнить несколько проверок и показать все провалы сразу |
assertIterableEquals |
сравнить коллекции поэлементно |
assertTimeout(Duration, ...) |
код должен уложиться во время |
Порядок аргументов важен: сначала ожидаемое, потом фактическое. Если перепутать, тест все равно поймает ошибку, но сообщение «expected … but was …» будет вводить в заблуждение.
Как выглядит упавший тест
Неверный код — допустим, скидку переписали «короче»:
return priceKopecks * (100 - percent) / 100;
Результат прогона тех же тестов:
│ ├─ "999" коп. - "50"% = "500" коп. ✘ expected: <500> but was: <499>
│ └─ "1" коп. - "99"% = "1" коп. ✘ expected: <1> but was: <0>
...
=> org.opentest4j.AssertionFailedError: expected: <500> but was: <499>
PriceCalculatorTest.manyPrices(PriceCalculatorTest.java:48)
Обе формулы делят нацело, но округляют разное: для неотрицательной цены исходная округляет вниз сумму скидки, новая — итоговую цену. Параметризованный тест с краевыми значениями (нечетная цена, 1 копейка) поймал смену бизнес-правила, которую тест на круглых 1000 рублях пропустил бы. Исправление — вернуть исходную формулу или, если правило округления действительно меняется, осознанно поменять ожидания в тесте.
Запуск в Maven
Минимальный pom.xml: версия JUnit задается один раз через BOM, остальные артефакты ее наследуют.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.example</groupId>
<artifactId>price</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
</plugin>
</plugins>
</build>
</project>
Код — в src/main/java, тесты — в src/test/java. Команды:
mvn test
mvn test -Dtest="PriceCalculatorTest#tenPercent"
Первая запускает все тесты, вторая — один метод. Вывод mvn test (Maven 3.9.16):
[INFO] Running PriceCalculatorTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.377 s -- in PriceCalculatorTest
[INFO] BUILD SUCCESS
Версию surefire лучше указывать явно: плагин из «умолчаний» старых версий Maven может не знать про JUnit Platform и не найти ни одного теста.
Запуск в Gradle
В build.gradle.kts (Kotlin DSL):
dependencies {
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
Запуск — ./gradlew test, один тест — ./gradlew test --tests "PriceCalculatorTest.tenPercent". Строка useJUnitPlatform() обязательна: без нее Gradle ищет тесты старым механизмом JUnit 4.
Если не получилось
Тестов 0, сборка зеленая. Частые причины: в Gradle нет useJUnitPlatform(); в Maven слишком старый surefire; импорт org.junit.Test из JUnit 4 вместо org.junit.jupiter.api.Test; тестовый класс лежит в src/main/java.
UnsupportedClassVersionError. JUnit 6 запускается на Java ниже 17. Так выглядит запуск JUnit 6.1.3 на Java 12:
java.lang.UnsupportedClassVersionError: org/junit/platform/console/ConsoleLauncher has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 56.0
Версия класса 61 — это Java 17. Решение: обновить JDK сборки или остаться на JUnit 5.14.x.
Ошибка у @BeforeAll. Неверно:
@BeforeAll
void connect() { // ошибка: без static
System.out.println("connect");
}
Результат — JUnit 6 не запускает ни одного теста класса (консольный лаунчер завершается с кодом 1, то есть сборка красная) и пишет:
[ERROR] @BeforeAll method 'void LifecycleTest.connect()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
Исправление — сделать метод static void connect() или поставить над классом @TestInstance(TestInstance.Lifecycle.PER_CLASS), если нужен общий экземпляр на все тесты.
Выводы
- JUnit — стандартный фреймворк автотестов в Java; на сентябрь 2026 актуален JUnit 6 (Java 17+), JUnit 5.14.x — для проектов на Java 8-16.
- Тест — метод с
@Test, проверка — статический метод изAssertions; сначала ожидаемое значение, потом фактическое. @BeforeEachготовит данные перед каждым тестом,@BeforeAll— один раз и должен бытьstatic.@ParameterizedTestс краевыми значениями ловит ошибки, которые пропускает один «круглый» пример.- Для запуска в Maven нужен JUnit BOM и современный surefire, в Gradle —
useJUnitPlatform().
Где применяется / связь с практикой
Освойте тему на практике
JUnit — основа автотестов в Java-проектах: на нем строятся проверки бизнес-логики, интеграционные тесты со Spring и UI-тесты на Selenium. Для тестировщика это стартовая точка автоматизации: написать тест, прочитать отчет о падении, подключить фреймворк к сборке. Эти навыки с нуля разбираются на курсе «Автоматизатор тестирования на Java. Базовый уровень», а бесплатно познакомиться с темой можно на открытых уроках Otus.
FAQ
Можно ли держать в одном проекте тесты JUnit 4 и JUnit 6?
Да, если подключить движок junit-vintage-engine: платформа запустит оба вида тестов. Но в JUnit 6 Vintage помечен устаревшим, поэтому это вариант на время миграции, а не постоянная схема.
Чем JUnit отличается от TestNG?
Оба решают одну задачу; TestNG исторически сильнее в группах и зависимостях между тестами, JUnit — де-факто стандарт в экосистеме Spring и в шаблонах сборки. Для нового проекта без особых требований обычно берут JUnit.
Нужен ли Mockito для тестов на JUnit?
Не всегда. Mockito подключают, когда тестируемый класс зависит от внешних сервисов, базы или сети, и эти зависимости нужно подменить моками — управляемыми объектами-двойниками; чистую логику, как в примере выше, тестируют без него.



