Жизненный цикл теста
Тесты одного класса часто начинаются одинаково: создать репозиторий, создать сервис. Копировать эти строки в каждый метод утомительно. JUnit позволяет вынести подготовку в отдельный метод, который выполняется перед каждым тестом, а уборку — в метод после него. Это методы жизненного цикла.
Сервис блога
Теперь тестируем сервис блога. Ему нужен репозиторий постов и способ уведомить автора. Оба — интерфейсы, поэтому в тестах их можно подменить:
// src/main/java/com/example/blog/PostRepository.java
package com.example.blog;
import java.util.List;
import java.util.Optional;
public interface PostRepository {
Optional<Post> findById(long id);
List<Post> findAll();
Post save(Post post);
void deleteById(long id);
}// src/main/java/com/example/blog/Notifier.java
package com.example.blog;
public interface Notifier {
void send(String to, String message);
}// src/main/java/com/example/blog/PostService.java
package com.example.blog;
import java.util.List;
public class PostService {
private final PostRepository repository;
private final Notifier notifier;
public PostService(PostRepository repository, Notifier notifier) {
this.repository = repository;
this.notifier = notifier;
}
public Post create(String title, String author) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Заголовок не может быть пустым");
}
return repository.save(new Post(null, title.strip(), author, false));
}
public Post find(long id) {
return repository.findById(id)
.orElseThrow(() -> new PostNotFoundException(id));
}
public Post publish(long id) {
Post post = find(id).publish();
repository.save(post);
notifier.send(post.author(), "Ваш пост «" + post.title() + "» опубликован");
return post;
}
public List<Post> published() {
return repository.findAll().stream()
.filter(Post::published)
.toList();
}
}PostNotFoundException наследует RuntimeException с сообщением "Пост не найден: " + id. Для тестов нужен ещё простой репозиторий, который хранит посты в памяти и выдаёт id 1, 2, 3 при save():
// src/main/java/com/example/blog/InMemoryPostRepository.java
package com.example.blog;
import java.util.*;
public class InMemoryPostRepository implements PostRepository {
private final Map<Long, Post> posts = new LinkedHashMap<>();
private long nextId = 1;
public Optional<Post> findById(long id) { return Optional.ofNullable(posts.get(id)); }
public List<Post> findAll() { return new ArrayList<>(posts.values()); }
public Post save(Post post) {
Post saved = post.id() == null
? new Post(nextId++, post.title(), post.author(), post.published())
: post;
posts.put(saved.id(), saved);
return saved;
}
public void deleteById(long id) { posts.remove(id); }
}@BeforeEach
// src/test/java/com/example/blog/PostServiceTest.java
package com.example.blog;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PostServiceTest {
private InMemoryPostRepository repository;
private List<String> sent;
private PostService service;
@BeforeEach
void setUp() {
repository = new InMemoryPostRepository();
sent = new ArrayList<>();
service = new PostService(repository, (to, message) -> sent.add(to + ": " + message));
}
@Test
void createSavesDraft() {
Post post = service.create(" Привет, JUnit ", "anna");
assertEquals(1L, post.id());
assertEquals("Привет, JUnit", post.title());
assertEquals(1, repository.findAll().size());
}
@Test
void publishSendsNotification() {
Post post = service.create("Привет, JUnit", "anna");
service.publish(post.id());
assertTrue(service.find(post.id()).published());
assertEquals(List.of("anna: Ваш пост «Привет, JUnit» опубликован"), sent);
}
}setUp() выполняется перед каждым тестом, поэтому оба теста получают пустой репозиторий и пустой список сообщений. Вместо почтового сервера сервис получает лямбду, которая складывает сообщения в список sent, — самодельную заглушку. В главе 4 такие заглушки будет создавать Mockito.
Все четыре аннотации
| Аннотация | Когда выполняется | Метод |
|---|---|---|
@BeforeEach | Перед каждым тестом | Обычный |
@AfterEach | После каждого теста, даже упавшего | Обычный |
@BeforeAll | Один раз перед всеми тестами класса | static |
@AfterAll | Один раз после всех тестов класса | static |
Если добавить все четыре с System.out.println, mvn test покажет порядок:
[INFO] Running com.example.blog.PostServiceTest
beforeAll: один раз на класс
setUp
тест: publishSendsNotification
tearDown
setUp
тест: createSavesDraft
tearDown
afterAll: один раз на классВидно две вещи. Порядок тестов JUnit выбирает сам — не в порядке следования в файле. И JUnit создаёт новый экземпляр тестового класса для каждого теста, так что поля и так свежие; @BeforeEach — удобное место, чтобы их заполнить. @AfterEach нужен для освобождения ресурсов: закрыть соединение, удалить файл.
Частая ошибка
@BeforeAll выполняется, когда ни одного экземпляра класса ещё нет, поэтому метод должен быть static. Если забыть, не запустится ни один тест класса:
[ERROR] TestEngine with ID 'junit-jupiter' encountered a critical issue during test discovery:
(1) [ERROR] @BeforeAll method 'void com.example.blog.BeforeAllTest.setUp()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
...
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD FAILUREДобавьте static или используйте @BeforeEach, если подготовку нужно повторять для каждого теста.