Понятные имена и вложенные классы
Когда тестов становится много, плоский список методов в одном классе читать трудно. В JUnit есть два инструмента. @DisplayName даёт тесту понятное имя с пробелами и любыми символами, в том числе по-русски. @Nested группирует тесты во внутренние классы по ситуациям: «при создании поста», «при публикации поста». Вместе они превращают отчёт о тестах в описание того, как ведёт себя класс.
@DisplayName
// src/test/java/com/example/blog/PostServiceNestedTest.java
@Test
@DisplayName("отклоняет пустой заголовок")
void rejectsEmptyTitle() {
assertThrows(IllegalArgumentException.class, () -> service.create(" ", "anna"));
}IDE показывает «отклоняет пустой заголовок» вместо имени метода. Имя метода по-прежнему важно: Maven использует его в отчётах, и вы видите его в стеке вызовов.
@Nested
Вложенный тестовый класс — нестатический внутренний класс с аннотацией @Nested. Весь тестовый класс:
// src/test/java/com/example/blog/PostServiceNestedTest.java
package com.example.blog;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("PostService")
class PostServiceNestedTest {
private PostService service;
@BeforeEach
void setUp() {
service = new PostService(new InMemoryPostRepository(), (to, message) -> { });
}
@Nested
@DisplayName("при создании поста")
class Create {
@Test
@DisplayName("обрезает пробелы в заголовке")
void trimsTitle() {
assertEquals("Привет", service.create(" Привет ", "anna").title());
}
@Test
@DisplayName("отклоняет пустой заголовок")
void rejectsEmptyTitle() {
assertThrows(IllegalArgumentException.class, () -> service.create(" ", "anna"));
}
}
@Nested
@DisplayName("при публикации поста")
class Publish {
private Post draft;
@BeforeEach
void createDraft() {
draft = service.create("Привет", "anna");
}
@Test
@DisplayName("помечает пост опубликованным")
void marksPublished() {
assertTrue(service.publish(draft.id()).published());
}
@Test
@DisplayName("падает для несуществующего id")
void unknownId() {
assertThrows(PostNotFoundException.class, () -> service.publish(99));
}
}
}Перед тестом из Publish JUnit сначала вызывает внешний setUp(), потом внутренний createDraft(). Так каждая группа добавляет свою подготовку поверх общей. Уведомитель здесь — пустая лямбда: эти тесты уведомления не проверяют.
Что вы увидите
В IntelliJ IDEA окно запуска покажет дерево:
PostService
├─ при создании поста
│ ├─ обрезает пробелы в заголовке
│ └─ отклоняет пустой заголовок
└─ при публикации поста
├─ помечает пост опубликованным
└─ падает для несуществующего idMaven печатает отображаемые имена групп:
[INFO] Running PostService
[INFO] Running при создании поста
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s -- in при создании поста
[INFO] Running при публикации поста
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.010 s -- in при публикации поста
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.026 s -- in PostServiceУ самого внешнего класса тестов нет, отсюда Tests run: 0 в последней строке — четыре теста посчитаны в группах. Если в консоли Windows вместо русских букв видны «кракозябры», это кодировка терминала, а не ошибка тестов.
Когда это нужно
Вложенные классы удобны, когда у класса несколько методов, у каждого свой набор случаев, или когда у тестов общее начальное состояние («есть черновик»). Для класса с тремя тестами плоский список проще.
Частая ошибка
Внутренний класс не должен быть static. Статический вложенный класс с @Nested не запускается внутри внешнего, и Surefire не запускает его сам по себе:
WARNING: TestEngine with ID 'junit-jupiter' encountered a non-critical issue during test discovery:
(1) [WARNING] @Nested class 'com.example.blog.StaticNestedTest$Create' must not be static. It will only be executed if discovered as a standalone test class. You should remove the annotation or make it non-static to resolve this warning.
...
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0Уберите static — тогда класс получит доступ к полям внешнего класса, например к service.