JUnit — тестирование Java-приложений

JUnit — стандартный фреймворк для модульного тестирования в Java. В этом курсе мы разберём JUnit 5 (Jupiter): от первого теста до параметризованных проверок и интеграции с Mockito.

Уровень: средний 19 разделов ⏱ 3–4 часа

1. Что такое JUnit и зачем тестировать

Тестирование — это процесс проверки того, что код работает корректно. JUnit позволяет писать автоматические тесты, которые запускаются при каждом изменении и мгновенно сообщают об ошибках.

Преимущества модульного тестирования:

JUnit 4 vs JUnit 5 (Jupiter)

ОсобенностьJUnit 4JUnit 5 (Jupiter)
Пакетorg.junitorg.junit.jupiter.api
Аннотация теста@Test@Test
Игнорирование теста@Ignore@Disabled
Диапазон значений@RunWith + @Parameterized@ParameterizedTest
Вложенные тестыне поддерживается@Nested
Расширения@RunWith + Rule@ExtendWith
Минимальная версия JavaJava 5+Java 8+
Начинай всегда с JUnit 5. Версия Jupiter полностью совместима с JUnit 4 через слой совместимости, поэтому старые тесты продолжат работать.

Жизненный цикл теста

  1. @BeforeAll — выполняется один раз перед всеми тестами класса.
  2. @BeforeEach — выполняется перед каждым тестовым методом.
  3. @Test — сам тест.
  4. @AfterEach — выполняется после каждого тестового метода.
  5. @AfterAll — выполняется один раз после всех тестов класса.

2. Подключение JUnit

Maven dependency

Добавь в секцию <dependencies> файла pom.xml:

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

Для Maven 3.8.5+ можно использовать BOM и не указывать версию:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.10.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Убедись, что плагин surefire настроен (обычно Maven добавляет его автоматически):

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

Структура папок

Следуй стандартной структуре Maven:

src/
  main/
    java/
      com/example/calculator/
        Calculator.java
  test/
    java/
      com/example/calculator/
        CalculatorTest.java
Файлы тестов должны находиться в папке src/test/java и по умолчанию содержать в имени слово Test (например, CalculatorTest.java).

Запуск

mvn test

В IntelliJ IDEA нажми зелёный треугольник рядом с тестовым методом или классом. Выборочная проверка доступна через контекстное меню.

3. Первый тест

Создадим класс Calculator и его тест:

public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {

    @Test
    void add_returnsSum() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void subtract_returnsDifference() {
        Calculator calc = new Calculator();
        assertEquals(4, calc.subtract(7, 3));
    }

    @Test
    void multiply_returnsProduct() {
        Calculator calc = new Calculator();
        assertEquals(12, calc.multiply(3, 4));
    }

    @Test
    void divide_returnsQuotient() {
        Calculator calc = new Calculator();
        assertEquals(3, calc.divide(9, 3));
    }

    @Test
    void divide_byZero_throwsException() {
        Calculator calc = new Calculator();
        assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
    }
}

Ключевые моменты:

4. Аннотации жизненного цикла

Аннотации позволяют выполнять код до и после тестов — например, для настройки базы данных или очистки ресурсов.

АннотацияКогда выполняетсяОграничения
@BeforeAllОдин раз перед всеми тестамиМетод должен быть static (или static в @TestInstance)
@BeforeEachПеред каждым тестомНет ограничений
@AfterEachПосле каждого тестаНет ограничений
@AfterAllОдин раз после всех тестовМетод должен быть static
@Disabled("причина")Пропуск тестаУкажи причину в параметре

Пример с таймером и счётчиком:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class LifecycleTest {

    private Calculator calc;

    @BeforeAll
    static void initSuite() {
        System.out.println("=== Все тесты запущены ===");
    }

    @BeforeEach
    void setUp() {
        calc = new Calculator();
        System.out.println("Новый экземпляр Calculator создан");
    }

    @AfterEach
    void tearDown() {
        System.out.println("Тест завершён");
    }

    @AfterAll
    static void finishSuite() {
        System.out.println("=== Все тесты пройдены ===");
    }

    @Test
    void testAdd() {
        assertEquals(10, calc.add(4, 6));
    }

    @Test
    void testMultiply() {
        assertEquals(20, calc.multiply(4, 5));
    }
}
Аннотация @TestInstance(Lifecycle.PER_CLASS) позволяет использовать нестатические @BeforeAll и @AfterAll, создавая один экземпляр тестового класса на весь набор тестов.

5. Проверки и ассерты

JUnit предоставляет семейство статических методов для проверки ожидаемых результатов. Все они находятся в классе Assertions.

Основные ассерты

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class AssertionTest {

    @Test
    void exampleAssertions() {
        Calculator calc = new Calculator();

        assertEquals(6, calc.add(2, 4), "Сумма 2+4 должна быть 6");

        assertTrue(calc.add(1, 1) > 0, "Результат должен быть положительным");

        assertNotNull(calc, "Объект не должен быть null");

        assertArrayEquals(
            new int[]{3, 6, 9},
            new int[]{3, 6, 9}
        );

        assertThrows(ArithmeticException.class, () -> calc.divide(5, 0));
    }
}

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

@Test
void divide_byZero_throwsException() {
    Calculator calc = new Calculator();
    ArithmeticException ex = assertThrows(
        ArithmeticException.class,
        () -> calc.divide(10, 0)
    );
    assertEquals("Division by zero", ex.getMessage());
}

Проверка времени выполнения

@Test
void heavyOperation_mustFinishInTime() {
    assertTimeout(java.time.Duration.ofSeconds(2), () -> {
        Thread.sleep(100);
        new Calculator().add(1, 1);
    });
}

Параметр message во всех ассертах опционален, но помогает при диагностике ошибок.

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

Параметризованные тесты позволяют запускать один и тот же тест с набором данных, исключая дублирование кода.

@ValueSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorParamTest {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void addOne_alwaysIncreases(int value) {
        Calculator calc = new Calculator();
        assertEquals(value + 1, calc.add(value, 1));
    }
}

@MethodSource

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorMethodTest {

    static Stream<Arguments> addPairs() {
        return Stream.of(
            Arguments.of(1, 2, 3),
            Arguments.of(0, 0, 0),
            Arguments.of(-1, 1, 0),
            Arguments.of(10, 20, 30)
        );
    }

    @ParameterizedTest
    @MethodSource("addPairs")
    void add_variousPairs(int a, int b, int expected) {
        Calculator calc = new Calculator();
        assertEquals(expected, calc.add(a, b));
    }
}

@CsvSource

@ParameterizedTest
@CsvSource({
    "10, 2, 5",
    "20, 4, 5",
    "100, 10, 10",
    "7, 3, 2"
})
void divide_variousPairs(int a, int b, int expected) {
    Calculator calc = new Calculator();
    assertEquals(expected, calc.divide(a, b));
}
Используй @CsvSource для простых наборов данных, @MethodSource — когда нужны сложные объекты или вычисления. @ValueSource удобен для проверки одного аргумента.

7. Вложенные тесты и именование

Аннотация @Nested группирует связанные тесты в логические блоки. @DisplayName задаёт человекочитаемое имя.

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Калькулятор")
public class CalculatorNestedTest {

    private final Calculator calc = new Calculator();

    @Nested
    @DisplayName("Сложение")
    class Addition {

        @Test
        @DisplayName("складывает два положительных числа")
        void add_positiveNumbers() {
            assertEquals(5, calc.add(2, 3));
        }

        @Test
        @DisplayName("работает с отрицательными числами")
        void add_negativeNumbers() {
            assertEquals(-5, calc.add(-2, -3));
        }
    }

    @Nested
    @DisplayName("Деление")
    class Division {

        @Test
        @DisplayName("возвращает целую часть")
        void divide_integerResult() {
            assertEquals(3, calc.divide(9, 3));
        }

        @Test
        @DisplayName("выбрасывает исключение при делении на ноль")
        void divide_byZero() {
            assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
        }
    }
}

В отчёте тесты отображаются в виде дерева: Калькулятор > Сложение > складывает два положительных числа. Это значительно улучшает читаемость.

8. Mockito — моки для зависимостей

Моки (mocks) подменяют реальные объекты в тестах, изолируя тестируемый код от внешних зависимостей: базы данных, API, файловой системы.

Добавь зависимость в pom.xml:

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

Допустим, у нас есть сервис и репозиторий:

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String getUserName(long id) {
        String name = userRepository.findNameById(id);
        if (name == null) {
            throw new IllegalArgumentException("User not found");
        }
        return name.toUpperCase();
    }
}
public interface UserRepository {
    String findNameById(long id);
}

Тест с моком:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void getUserName_returnsUpperCase() {
        when(userRepository.findNameById(1L)).thenReturn("anna");

        String result = userService.getUserName(1L);

        assertEquals("ANNA", result);
        verify(userRepository, times(1)).findNameById(1L);
    }

    @Test
    void getUserName_throwsWhenNotFound() {
        when(userRepository.findNameById(99L)).thenReturn(null);

        assertThrows(IllegalArgumentException.class, () -> userService.getUserName(99L));
    }
}

Основные методы Mockito:

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

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

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

После запуска mvn test открой отчёт:

target/site/jacoco/index.html

Как читать отчёт JaCoCo

МетрикаОписание
InstructionsДоля.bytecode-инструкций, покрытых тестами
BranchesПокрытие ветвлений (if/switch)
LinesПокрытие строк кода
MethodsПокрытие методов
ClassesПокрытие классов
Цель — не 100% покрытие, а осмысленное тестирование критичной логики. 80% покрытия с хорошими тестами лучше, чем 100% с тривиальными проверками.

10. Тестирование Spring-приложений

Spring Boot предоставляет встроенные инструменты для интеграционного тестирования.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class ApplicationTests {

    @Autowired
    private ApplicationContext context;

    @Test
    void contextLoads() {
        assertNotNull(context);
    }
}

Для тестирования REST-контроллеров без запуска сервера:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest
@AutoConfigureMockMvc
public class CalculatorControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void getAdd_returnsJson() throws Exception {
        mockMvc.perform(get("/api/add?a=2&b=3"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.result").value(5));
    }
}

Для подмены бинов Spring используй @MockBean вместо @Mock из Mockito.

Подробное руководство по тестированию Spring Boot — в нашем курсе Spring Boot.

11. Лучшие практики

AAA-паттерн

Каждый тест должен чётко следовать трём шагам:

@Test
void add_twoPositiveNumbers_returnsSum() {
    // Arrange
    Calculator calc = new Calculator();
    int a = 3;
    int b = 4;
    int expected = 7;

    // Act
    int result = calc.add(a, b);

    // Assert
    assertEquals(expected, result);
}

Рекомендации

Не создавай тесты, которые просто вызывают метод без проверки. Такой тест всегда пройдёт, но не обнаружит ошибок.

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

Проверка того, что метод правильно бросает исключение, — такой же сценарий, как и обычные вычисления. JUnit 5 предлагает три способа, и каждый уместен в своей ситуации.

@Test
void divideByZero_throwsException() {
    ArithmeticException ex = assertThrows(
        ArithmeticException.class,
        () -> calculator.divide(10, 0)
    );
    assertEquals("/ by zero", ex.getMessage());
}

Альтернатива — проверка с возвратом исключения для дополнительных проверок:

@Test
void wrongPassword_throwsAuthenticationException() {
    AuthenticationException ex = assertThrows(
        AuthenticationException.class,
        () -> auth.login("user", "bad")
    );
    assertTrue(ex.getMessage().contains("пароль"));
}
Используй assertThrows — он читается однозначно и не требует пустых катчей.
Ловить Exception.class — плохая практика: слишком широкое ожидание может скрыть неожиданные ошибки.

13. Порядок и жизненный цикл тестов

По умолчанию JUnit создаёт новый экземпляр тестового класса для каждого метода. За это отвечает @TestInstance(Lifecycle.PER_METHOD) — поведение по умолчанию.

@TestInstance(Lifecycle.PER_CLASS)
class CalculatorServiceTest {

    private final CalculatorService service;

    CalculatorServiceTest() {
        service = new CalculatorService();
    }

    @Test
    void add_returnsSum() {
        assertEquals(7, service.add(3, 4));
    }
}

При PER_CLASS экземпляр один: можно использовать поля без static, а конструктор «заранее» подготовить объект. Подходит для медленной инициализации.

РежимЭкземпляровКогда удобно
PER_METHODодин на тестизоляция по умолчанию
PER_CLASSодин на класстяжёлая инициализация
С PER_CLASS поля не сбрасываются между тестами. Если тесты зависят от порядка — это повод подумать о дизайне.

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

Проверить метод на разных входных данных можно в одном тесте. JUnit 5 для этого имеет @ParameterizedTest и источники аргументов.

@ParameterizedTest
@CsvSource({
    "2, 3, 5",
    "0, 0, 0",
    "-1, 1, 0"
})
void add_returnsSum(int a, int b, int expected) {
    assertEquals(expected, calculator.add(a, b));
}
@ParameterizedTest
@MethodSource("validEmails")
void validEmails_areAccepted(String email) {
    assertTrue(validator.isEmail(email));
}

static Stream<String> validEmails() {
    return Stream.of("a@b.ru", "user@example.com", "x@y.io");
}

Другие источники: @ValueSource — простые значения, @EnumSource — перечисления, @NullSource и @EmptySource — пограничные случаи.

Комбинируй @NullSource и @EmptySource с @ValueSource: тривиальные ошибки нулевых ссылок ловятся на раз.

15. Интеграционные тесты и Testcontainers

Модульные тесты изолируют логику, но для проверки связки «приложение + БД + внешний сервис» нужны интеграционные. Тестованный контейнер поднимает Docker-образ и тестирует в реальных условиях.

@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16")
            .withDatabaseName("test");

    @Autowired
    UserRepository userRepository;

    @Test
    void saves_and_finds_user() {
        userRepository.save(new User("Иван"));
        assertEquals(1, userRepository.findAll().size());
    }
}

Контейнер поднимается один раз на класс, а URL из его порта подставляется в datasource — через spring.test.replace или динамические свойства.

Детальнее — курсы Testcontainers и Spring Boot. Для лёгких проверок SQL подходит H2.

16. AssertJ: выразительные ассерты

Цепочки AssertJ делают проверки читаемее, чем набор atomic assert: код сам описывает ожидаемое состояние.

import static org.assertj.core.api.Assertions.*;

assertThat(user.getEmail()).isEqualTo("i@example.com")
    .contains("@").endsWith(".com");
assertThat(result.getTotal()).isGreaterThan(0)
    .isLessThanOrEqualTo(100);
assertThat(list).extracting(User::getName)
    .containsExactly("Ирина", "Олег");
Подключайте статический импорт в едином файле — код читается как «установленное правило».
Помните: AssertJ не заменяет assertThrows для исключений — для них используйте assertThatThrownBy(...) с проверкой причин (разделы 5, 12).

17. Тестирование веб-слоя: MockMvc и RestAssured

Spring-слой контроллеров тестируют без запуска всего приложения через MockMvc (mock сервлет) или RestAssured (реальные HTTP-запросы к запущенному сервису).

@WebMvcTest(UserController.class)
class UserControllerTest {
  @Autowired MockMvc mvc;

  @Test
  void returnsUser() throws Exception {
    mvc.perform(get("/api/users/1"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$.name").isString());
  }
}
Комбинация: 80% — MockMvc, для интеграционных — Testcontainers (раздел 15) + RestAssured к запущенному сервису.
Не забывайте мокать зависимости контроллера (@MockBean) — иначе тест случайно потянет базу.

18. Параллельный запуск тестов и @Execution

Современные машины легко гоняют тесты параллельно. JUnit 5 поддерживает параллельность по конфигурации, JUnit 4 — только плагинами.

// junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent

// на уровне класса
@Execution(ExecutionMode.CONCURRENT)
class ParallelTests { ... }
Сначала прогоните скесительный прогон серийно и параллельно: если результаты совпадают, включайте постоянно.
Параллельность в JUnit не «ускоряет магию»: тесты с общей БД или файлами конфликтуют. Изолируйте ресурсы до включения.

19. Мутационное тестирование с PIT

Обычное покрытие показывает, какие строки выполнения тестов, но не что тесты заметят ошибку. PIT (pitest) меняет код (мутации) и проверяет, упадут ли тесты — так видна «серая» зона.

// pom.xml — плагин pitest-maven
<plugin>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <version>1.15.0</version>
</plugin>

mvn org.pitest:pitest-maven:mutationCoverage
МетрикаСмысл
Mutation Scoreдоля «убейте мутанта» тестами
Survivedмутации, не замеченные тестами
Killedмутации, обнаруженные
Стремитесь к mutation score 80%+ на бизнес-логике, не гоняйте точность в маргинальных сетерах.
PIT требует достоверных тестов без побочных эффектов, иначе он «замутит» ложные мутации в интеграциях. Запускайте на чистом окружении.

20. Awaitility: тесты времени и асинхронности

Проверка асинхронного кода через «положди 3 секунды» — источник флаки-тестов: на слабой машине трёх мало, на быстрой — много. Awaitility ждёт условие с таймаутом и опросом.

await().atMost(5, SECONDS)
       .pollInterval(100, MILLISECONDS)
       .until(() -> cache.contains("token"));

// если условие не наступает за отведённое
// время — тест падает с понятным описанием
Отлично подходит для @Scheduled-задач: проверяйте, что задача применила изменения по условию, а не по «прошло 2 секунды».
Awaitility не подменяет реальные тайминги: если фоновый таймер настоящий, в тесте отключайте scheduled-задачи профилем, иначе случайные срабатывания испортят ожидание.

21. Тестирование файлов, IO и кодировок

Парсеры CSV/JSON, чтение конфигов, работа с большими файлами — классические кандидаты на тесты. Здесь важны тестовые данные и фикстуры не меньше самого кода.

@TempDir Path tempDir;   // JUnit 5: автоудаление

@Test
void parsesUtf8() throws Exception {
  Path f = tempDir.resolve("in.csv");
  Files.writeString(f, "имя,сумма\nИван,100", UTF_8);
  List rows = parser.parse(f);
  assertEquals("Иван", rows.get(0).name());
}
Для больших файлов генерируйте данные (100 тыс. строк), а не «полный» файл: тест останется быстрым и воспроизводимым.
Тест, читающий файл «по пути из реальной папки», не переживёт CI: любой ресурс — через classpath или @TempDir.

22. BDD и спецификации: Cucumber

BDD переносит тесты на язык бизнеса: Given/When/Then описывает поведение, а шаги превращаются в код. Требования и реализация живут в одном артефакте.

Feature: Скидка для суммы
  Scenario: Больше 1000 — скидка 10%
    Given корзина с суммой 1200
    When считаю скидку
    Then результат равен 120

@Given("корзина с суммой {int}")
public void cart(int sum) { cart = sum; }
Не превращайте каждый юнит-тест в BDD: Cucumber оправдан для бизнес-правил и интеграций, а нижний слой остаётся на JUnit.
Сценарии, оторванные от реальных данных, — «театр»: фикстуру Given держите минимальной, чтобы сценарий оставался и читаемым, и поддерживаемым.

23. Тестирование унаследованного кода (legacy)

Покрыть чужой «тёмный» код тестами и не сломаться — отдельный навык. Правило: сначала санитарная сеть на текущее поведение, затем рефакторинг — по одному шагу за раз.

// 1. тест-«слепок» фактического поведения
@Test
void preservedBehaviour() {
  Legacy legacy = new Legacy();
  assertEquals("server-1:8080",
       legacy.buildHost("1", "8080"));
}
// 2. затем правьте логику, сверяясь с тестом.
Покрывайте сначала самые «дорогие» функции (много вызовов, высокая цена бага), а не самые короткие — эффект быстрее.
Тест, «зафиксировавший» баг как норму, — ловушка: помечайте его пометкой «ожидаемое поведение до правки» и возвращайтесь после рефакторинга.

Что дальше

Теперь ты умеешь писать модульные тесты в JUnit 5. Рекомендуемые следующие шаги: изучить Gradle для управления сборкой, углубиться в Spring Boot для тестирования веб-приложений и освоить интеграционные тесты с Testcontainers.

← Вернуться к курсам