Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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

Тесты одного класса часто начинаются одинаково: создать репозиторий, создать сервис. Копировать эти строки в каждый метод утомительно. 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, если подготовку нужно повторять для каждого теста.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.