Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Test lifecycle

Tests of one class often start the same way: create a repository, create a service. Copying these lines into every method is tedious. JUnit lets you put the preparation into a separate method that runs before each test — and the cleanup into one that runs after. These are the lifecycle methods.

The blog service

Now we test the blog service. It needs a post repository and a way to notify the author. Both are interfaces, so they can be replaced in tests:

// 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("Title must not be empty");
        }
        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(), "Your post \"" + post.title() + "\" is published");
        return post;
    }
 
    public List<Post> published() {
        return repository.findAll().stream()
                .filter(Post::published)
                .toList();
    }
}

PostNotFoundException extends RuntimeException with the message "Post not found: " + id. For tests we also need a simple repository that keeps posts in memory and assigns ids 1, 2, 3 on 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("  Hello, JUnit  ", "anna");
 
        assertEquals(1L, post.id());
        assertEquals("Hello, JUnit", post.title());
        assertEquals(1, repository.findAll().size());
    }
 
    @Test
    void publishSendsNotification() {
        Post post = service.create("Hello, JUnit", "anna");
 
        service.publish(post.id());
 
        assertTrue(service.find(post.id()).published());
        assertEquals(List.of("anna: Your post \"Hello, JUnit\" is published"), sent);
    }
}

setUp() runs before each test, so both tests get an empty repository and an empty list of messages. Instead of a mail server, the service receives a lambda that writes messages into the sent list — a hand-made stand-in. In chapter 4 Mockito will create such stand-ins for us.

All four annotations

AnnotationWhen it runsMethod
@BeforeEachBefore every testOrdinary
@AfterEachAfter every test, even a failed oneOrdinary
@BeforeAllOnce before all tests of the classstatic
@AfterAllOnce after all tests of the classstatic

If you add all four with System.out.println, mvn test shows the order:

[INFO] Running com.example.blog.PostServiceTest
beforeAll: once for the class
setUp
test: publishSendsNotification
tearDown
setUp
test: createSavesDraft
tearDown
afterAll: once for the class

Two things are visible. JUnit picks the order of tests itself — not the order in the file. And JUnit creates a new instance of the test class for every test, so the fields are fresh anyway; @BeforeEach is a convenient place to fill them. @AfterEach is for releasing resources: closing a connection, deleting a file.

Common mistake

@BeforeAll runs before any instance of the class exists, so the method must be static. If you forget, not a single test of the class runs:

[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

Add static, or use @BeforeEach if the preparation should be repeated for every test.

Thanks for reading!

Donate to the site — we work hard for you.