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
| Annotation | When it runs | Method |
|---|---|---|
@BeforeEach | Before every test | Ordinary |
@AfterEach | After every test, even a failed one | Ordinary |
@BeforeAll | Once before all tests of the class | static |
@AfterAll | Once after all tests of the class | static |
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 classTwo 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 FAILUREAdd static, or use @BeforeEach if the preparation should be repeated for every test.