JUnit в Java: аннотации, assertions и запуск тестов в JUnit 5 и 6

JUnit в Java: аннотации, assertions и запуск тестов в JUnit 5 и 6 Полезное

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 подключают, когда тестируемый класс зависит от внешних сервисов, базы или сети, и эти зависимости нужно подменить моками — управляемыми объектами-двойниками; чистую логику, как в примере выше, тестируют без него.

OTUS Журнал
Бесплатные открытые уроки (поп-ап)