Статьи

Полный курс по языку Java — от установки JDK до многопоточности и HTTP-запросов

Тестирование с JUnit

Страница 28 из 30 · 2026-10-25 · 7 мин чтения

Зачем писать тесты

Автоматические тесты проверяют, что код работает, и сразу сообщают, если после изменений что-то сломалось. Они же служат живой документацией: по тестам видно, как должен вести себя класс.

Основной фреймворк для Java — JUnit 5 (JUnit Jupiter).

Подключение

Maven (pom.xml):

<dependency>    <groupId>org.junit.jupiter</groupId>    <artifactId>junit-jupiter</artifactId>    <version>5.11.0</version>    <scope>test</scope></dependency>

Gradle (build.gradle.kts):

dependencies {    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")    testRuntimeOnly("org.junit.platform:junit-platform-launcher")}tasks.test {    useJUnitPlatform()}

Тесты лежат в src/test/java в тех же пакетах, что и тестируемые классы.

Первый тест

Тестируемый класс src/main/java/com/example/Calculator.java:

package com.example;public class Calculator {    public int add(int a, int b) {        return a + b;    }    public int divide(int a, int b) {        if (b == 0) {            throw new IllegalArgumentException("Деление на ноль");        }        return a / b;    }    public boolean isPrime(int n) {        if (n < 2) {            return false;        }        for (int i = 2; (long) i * i <= n; i++) {            if (n % i == 0) {                return false;            }        }        return true;    }}

Тест src/test/java/com/example/CalculatorTest.java:

package com.example;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertFalse;import static org.junit.jupiter.api.Assertions.assertThrows;import static org.junit.jupiter.api.Assertions.assertTrue;import org.junit.jupiter.api.Test;class CalculatorTest {    private final Calculator calculator = new Calculator();    @Test    void addsTwoNumbers() {        assertEquals(5, calculator.add(2, 3));    }    @Test    void addsNegativeNumbers() {        assertEquals(-5, calculator.add(-2, -3));    }    @Test    void dividesNumbers() {        assertEquals(4, calculator.divide(20, 5));    }    @Test    void divisionByZeroThrows() {        IllegalArgumentException e = assertThrows(            IllegalArgumentException.class,            () -> calculator.divide(1, 0)        );        assertEquals("Деление на ноль", e.getMessage());    }    @Test    void detectsPrimes() {        assertTrue(calculator.isPrime(13));        assertFalse(calculator.isPrime(15));        assertFalse(calculator.isPrime(1));    }}

Тестовые классы и методы могут быть без public. Имена методов должны описывать проверяемое поведение.

Запуск

mvn test./gradlew test

В IDE: зеленый треугольник рядом с классом или методом. Отчеты Maven находятся в target/surefire-reports, Gradle — в build/reports/tests/test/index.html.

Основные проверки

import static org.junit.jupiter.api.Assertions.*;import java.util.List;import org.junit.jupiter.api.Test;class AssertionsTest {    @Test    void variousAssertions() {        assertEquals(10, 5 + 5);        assertEquals(0.3, 0.1 + 0.2, 0.0001);        assertNotEquals(1, 2);        assertTrue(10 > 5);        assertFalse("abc".isEmpty());        assertNull(null);        assertNotNull("значение");        String a = "text";        String b = a;        assertSame(a, b);        assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});        assertIterableEquals(List.of(1, 2), List.of(1, 2));        assertEquals(4, 2 + 2, "Сообщение при провале теста");    }    @Test    void groupedAssertions() {        assertAll(            "проверка строки",            () -> assertEquals(5, "hello".length()),            () -> assertTrue("hello".startsWith("he")),            () -> assertEquals("HELLO", "hello".toUpperCase())        );    }}

Порядок аргументов assertEquals(ожидаемое, фактическое). Для double обязательно задавайте допустимую погрешность.

assertAll выполняет все проверки и показывает все провалившиеся, а не только первую.

Жизненный цикл

import org.junit.jupiter.api.AfterAll;import org.junit.jupiter.api.AfterEach;import org.junit.jupiter.api.BeforeAll;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;class LifecycleTest {    @BeforeAll    static void initAll() {        System.out.println("один раз перед всеми тестами");    }    @BeforeEach    void init() {        System.out.println("перед каждым тестом");    }    @Test    void first() {        System.out.println("тест 1");    }    @Test    void second() {        System.out.println("тест 2");    }    @AfterEach    void tearDown() {        System.out.println("после каждого теста");    }    @AfterAll    static void tearDownAll() {        System.out.println("один раз после всех тестов");    }}

Для каждого тестового метода JUnit создает новый экземпляр класса, поэтому тесты не должны зависеть друг от друга и от порядка выполнения.

Другие аннотации

АннотацияНазначение
@DisplayName("...")читаемое название теста
@Disabled("причина")временно отключить тест
@Nestedвложенный класс для группировки тестов
@Tag("slow")метка для фильтрации
@Timeout(2)ограничение времени в секундах
@RepeatedTest(5)повторить тест несколько раз
import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertTrue;import java.util.ArrayList;import java.util.List;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.DisplayName;import org.junit.jupiter.api.Nested;import org.junit.jupiter.api.Test;class ListTest {    private List<String> list;    @BeforeEach    void setUp() {        list = new ArrayList<>();    }    @Nested    @DisplayName("Когда список пуст")    class WhenEmpty {        @Test        @DisplayName("размер равен нулю")        void sizeIsZero() {            assertEquals(0, list.size());        }        @Test        @DisplayName("isEmpty возвращает true")        void isEmpty() {            assertTrue(list.isEmpty());        }    }    @Nested    @DisplayName("Когда в списке есть элементы")    class WhenNotEmpty {        @BeforeEach        void addItems() {            list.add("a");            list.add("b");        }        @Test        @DisplayName("размер равен двум")        void sizeIsTwo() {            assertEquals(2, list.size());        }    }}

Параметризованные тесты

Один тест для набора входных данных. Для них нужна зависимость junit-jupiter (она уже включает junit-jupiter-params).

import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertFalse;import static org.junit.jupiter.api.Assertions.assertTrue;import com.example.Calculator;import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.CsvSource;import org.junit.jupiter.params.provider.MethodSource;import org.junit.jupiter.params.provider.ValueSource;import java.util.stream.Stream;import org.junit.jupiter.params.provider.Arguments;class ParameterizedCalculatorTest {    private final Calculator calculator = new Calculator();    @ParameterizedTest    @ValueSource(ints = {2, 3, 5, 7, 11, 13})    void primesAreDetected(int n) {        assertTrue(calculator.isPrime(n));    }    @ParameterizedTest    @ValueSource(ints = {-5, 0, 1, 4, 9, 15})    void nonPrimesAreDetected(int n) {        assertFalse(calculator.isPrime(n));    }    @ParameterizedTest    @CsvSource({        "1, 2, 3",        "10, -5, 5",        "0, 0, 0"    })    void addsNumbers(int a, int b, int expected) {        assertEquals(expected, calculator.add(a, b));    }    @ParameterizedTest    @MethodSource("divisionCases")    void dividesNumbers(int a, int b, int expected) {        assertEquals(expected, calculator.divide(a, b));    }    static Stream<Arguments> divisionCases() {        return Stream.of(            Arguments.of(10, 2, 5),            Arguments.of(9, 3, 3),            Arguments.of(7, 2, 3)        );    }}

Другие источники данных: @EnumSource, @CsvFileSource, @NullSource, @EmptySource.

Тестирование исключений

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;import static org.junit.jupiter.api.Assertions.assertThrows;import org.junit.jupiter.api.Test;class ExceptionTest {    @Test    void parseThrowsOnInvalidInput() {        assertThrows(NumberFormatException.class, () -> Integer.parseInt("abc"));    }    @Test    void parseDoesNotThrowOnValidInput() {        assertDoesNotThrow(() -> Integer.parseInt("42"));    }}

Пример: тест своего класса

Класс:

package com.example;import java.util.ArrayList;import java.util.List;public class ShoppingCart {    public record Item(String name, double price, int quantity) {    }    private final List<Item> items = new ArrayList<>();    public void add(String name, double price, int quantity) {        if (price < 0) {            throw new IllegalArgumentException("Цена не может быть отрицательной");        }        if (quantity <= 0) {            throw new IllegalArgumentException("Количество должно быть положительным");        }        items.add(new Item(name, price, quantity));    }    public double total() {        return items.stream().mapToDouble(i -> i.price() * i.quantity()).sum();    }    public int count() {        return items.stream().mapToInt(Item::quantity).sum();    }    public boolean isEmpty() {        return items.isEmpty();    }}

Тест:

package com.example;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertThrows;import static org.junit.jupiter.api.Assertions.assertTrue;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;class ShoppingCartTest {    private ShoppingCart cart;    @BeforeEach    void setUp() {        cart = new ShoppingCart();    }    @Test    void newCartIsEmpty() {        assertTrue(cart.isEmpty());        assertEquals(0, cart.total());    }    @Test    void totalSumsAllItems() {        cart.add("Хлеб", 2.5, 2);        cart.add("Молоко", 1.2, 3);        assertEquals(8.6, cart.total(), 0.0001);        assertEquals(5, cart.count());    }    @Test    void rejectsNegativePrice() {        assertThrows(IllegalArgumentException.class, () -> cart.add("Товар", -1, 1));    }    @Test    void rejectsZeroQuantity() {        assertThrows(IllegalArgumentException.class, () -> cart.add("Товар", 1, 0));    }}

Принципы хороших тестов

  • Структура Arrange-Act-Assert: подготовка данных, вызов, проверка.
  • Независимость: тест не зависит от других тестов и внешнего состояния.
  • Одна идея на тест: при падении сразу понятно, что сломалось.
  • Понятные имена: divisionByZeroThrows, а не test1.
  • Быстрота: юнит-тесты должны выполняться за миллисекунды.
  • Граничные случаи: ноль, пустые данные, null, максимальные значения.
  • Тестируйте поведение, а не внутреннюю реализацию.

Моки

Когда класс зависит от внешнего сервиса, базы или сети, зависимость подменяют заглушкой. Самая популярная библиотека — Mockito:

<dependency>    <groupId>org.mockito</groupId>    <artifactId>mockito-core</artifactId>    <version>5.14.0</version>    <scope>test</scope></dependency>
package com.example;public interface PriceService {    double getPrice(String product);}
package com.example;public class OrderCalculator {    private final PriceService priceService;    public OrderCalculator(PriceService priceService) {        this.priceService = priceService;    }    public double calculate(String product, int quantity) {        return priceService.getPrice(product) * quantity;    }}
package com.example;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.mockito.Mockito.mock;import static org.mockito.Mockito.verify;import static org.mockito.Mockito.when;import org.junit.jupiter.api.Test;class OrderCalculatorTest {    @Test    void calculatesTotalUsingPriceService() {        PriceService service = mock(PriceService.class);        when(service.getPrice("кофе")).thenReturn(3.5);        OrderCalculator calculator = new OrderCalculator(service);        assertEquals(10.5, calculator.calculate("кофе", 3), 0.0001);        verify(service).getPrice("кофе");    }}

Если сможете обойтись без мока, например, простой реализацией интерфейса прямо в тесте, так лучше: код проще.

Подход TDD

Test-Driven Development: сначала пишется падающий тест, затем минимальный код, чтобы он прошел, затем рефакторинг. Цикл «красный → зеленый → рефакторинг» помогает проектировать код так, чтобы его было удобно проверять.

Покрытие кода

Показывает, какие строки выполнялись тестами. В IntelliJ IDEA: Run with Coverage. Для сборки используют плагин JaCoCo. Высокий процент покрытия не гарантирует качества, но низкий сигнализирует о непроверенном коде.