JAVA / PLATFORM, BUILDS AND TESTING
JUnit tests, assertions and lifecycle methods
Write JUnit 5 tests that assert behaviour precisely and use @BeforeEach, @AfterEach, @BeforeAll and @AfterAll to build and tear down fixtures correctly.
What you will learn
- Assert with assertEquals, assertThrows and assertAll instead of printing values
- Rebuild mutable fixtures in @BeforeEach; JUnit gives each test a new class instance
- Put cleanup in @AfterEach, which still runs after a failed assertion
- Read failures as 'label ==> expected: <x> but was: <y>' and keep expected first
Understanding JUnit tests, assertions and lifecycle methods
A Jupiter test is a no-argument method marked @Test: the engine discovers it, constructs the class, invokes the method, and records a pass if the invocation simply returns. Failure is signalled the only way a plain method can signal it, by throwing. The static methods on org.junit.jupiter.api.Assertions do exactly that, throwing an org.opentest4j.AssertionFailedError when a comparison does not hold. That is why a test body that only prints values is always green: printing never throws, so there is nothing for the engine to catch.
By default JUnit constructs a fresh instance of the test class for every @Test method, so instance fields cannot carry data from one test into the next. @BeforeEach runs on that new instance right after construction, and @AfterEach runs when the method finishes, including when an assertion has already thrown, which makes it the right home for releasing resources. @BeforeAll and @AfterAll bracket the whole class exactly once and must be static under that per-method lifecycle, because no single instance exists to own them. Treat these hooks as the mechanism that keeps tests independent: the engine does not promise to run methods in source order, so anything a test leaves behind in a static field is a bug waiting for a reordering.
Which assertion you choose decides how much the failure report can tell you. assertEquals takes the expected value first and prints both sides, assertThrows returns the exception it caught so you can go on to check its message, and assertAll runs several independent checks on the same object and reports every one that failed rather than stopping at the first. The optional last argument is a label that JUnit places before '==>' in the message; it adds context but never substitutes for the values, so collapsing a comparison into assertTrue throws that context away.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // fixes the order only so the log is stable
class CartTest {
static class Cart {
private int total;
private int items;
void add(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("price must be positive");
}
total += cents;
items++;
}
int total() { return total; }
int items() { return items; }
}
private Cart cart;
@BeforeAll
static void beforeAll() {
System.out.println("@BeforeAll: once, before any test");
}
@BeforeEach
void newCart() {
cart = new Cart();
System.out.println("@BeforeEach: fresh cart, items=" + cart.items());
}
@Test
@Order(1)
void sumsPrices() {
cart.add(1200);
cart.add(350);
assertEquals(1550, cart.total(), "total of 1200 and 350");
System.out.println(" sumsPrices body finished");
}
@Test
@Order(2)
void rejectsFreeItems() {
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> cart.add(0));
assertEquals("price must be positive", e.getMessage());
System.out.println(" rejectsFreeItems body finished");
}
@AfterEach
void afterEach() {
System.out.println("@AfterEach: discarding cart with items=" + cart.items());
}
@AfterAll
static void afterAll() {
System.out.println("@AfterAll: once, after every test");
}
}A JUnit test passes by not throwing, so what you assert and what the lifecycle hooks rebuild before each test is the entire test.
Worked examples
Assertions are ordinary methods that throw
Calls the Assertions API from a main method to show what a passing assertion returns and what a failing one puts in its message.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class AssertionsAreJustMethods {
static int cents(String euros) {
if (euros.isEmpty()) {
throw new IllegalArgumentException("empty amount");
}
return (int) Math.round(Double.parseDouble(euros) * 100);
}
public static void main(String[] args) {
assertEquals(1250, cents("12.50"));
System.out.println("a holding assertion returns normally");
try {
assertEquals(1200, cents("12.50"), "12.50 euros in cents");
} catch (AssertionError failure) {
System.out.println(failure.getClass().getName());
System.out.println(failure.getMessage());
}
IllegalArgumentException thrown =
assertThrows(IllegalArgumentException.class, () -> cents(""));
System.out.println("assertThrows returned: " + thrown.getMessage());
try {
assertThrows(IllegalArgumentException.class, () -> cents("1.00"));
} catch (AssertionError failure) {
System.out.println(failure.getMessage());
}
}
}Example explained
Line 1assertEquals(1250, cents("12.50")) produces no output at all: there is no 'pass' event, only the absence of a throw.
Line 2The failure object is org.opentest4j.AssertionFailedError, a subclass of AssertionError, so catching AssertionError shows exactly the text a report would print.
Line 3Your label lands before '==>' and never replaces the two values, so it should explain the intent instead of repeating the numbers.
Line 4assertThrows hands back the caught exception so a second assertion can check its message; when nothing is thrown, it fails with a message naming the type you expected.
One instance per test method
Proves that the default lifecycle builds a new test-class object for each @Test, which is why instance fields never leak between tests.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class FreshInstanceTest {
private static int instancesBuilt = 0;
private int beforeEachCalls = 0;
FreshInstanceTest() {
instancesBuilt++;
}
@BeforeEach
void count() {
beforeEachCalls++;
}
@Test
@Order(1)
void first() {
System.out.println("first: instancesBuilt=" + instancesBuilt
+ " beforeEachCalls=" + beforeEachCalls);
}
@Test
@Order(2)
void second() {
System.out.println("second: instancesBuilt=" + instancesBuilt
+ " beforeEachCalls=" + beforeEachCalls);
}
}Example explained
Line 1The constructor runs twice, so instancesBuilt climbs to 2 while beforeEachCalls stays at 1: each test got its own object.
Line 2A static field is the one thing that survives, which is exactly the shape of state that couples tests together.
Line 3@Order with @TestMethodOrder only pins the printing order; a healthy test class passes whatever order the engine picks.
Line 4These lines are the class's standard output; Surefire, Gradle or the ConsoleLauncher print their own pass/fail summary around them.
PER_CLASS lifecycle and the state it keeps
Shows how @TestInstance(Lifecycle.PER_CLASS) allows a non-static @BeforeAll and then lets one test see what an earlier test wrote.
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestMethodOrder;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SharedInstanceTest {
private final List<String> log = new ArrayList<>();
@BeforeAll
void openConnection() {
log.add("open");
}
@Test
@Order(1)
void writes() {
log.add("writes");
System.out.println("writes sees " + log);
}
@Test
@Order(2)
void reads() {
log.add("reads");
System.out.println("reads sees " + log);
}
}Example explained
Line 1PER_CLASS makes JUnit build a single instance for the class, so openConnection() may be an instance method rather than static.
Line 2The field initializer runs once, which is why 'open' is still present when the second test starts.
Line 3reads sees the entry writes added: nothing resets the shared object, so @BeforeEach becomes the only place a reset can happen.
Line 4Reserve this lifecycle for genuinely expensive fixtures and keep the shared state read-only.
Important notes
A failed assertion throws immediately, so statements after it in the test body never run; cleanup belongs in @AfterEach, which still executes after the failure.
assertEquals compares objects with equals(), so use assertArrayEquals for arrays and the three-argument delta overload for floating point: assertEquals(0.1 + 0.2, 0.3) fails.
Common mistakes
Writing assertEquals(cart.total(), 1550) instead of assertEquals(1550, cart.total()): when total() returns 1400 the report says expected 1400 but was 1550, so the constant looks like the faulty value and you debug the test rather than the code.
Replacing a comparison with assertTrue(cart.total() == 1550) or assertTrue(a.equals(b)): the failure only says expected <true> but was <false>, and you must rerun under a debugger to learn the actual value the assertion already had.
Keeping the fixture in a static field or having one @Test build state another asserts on: the pair passes while the engine happens to run them in that order and breaks after a rename, a filter, or parallel execution.
Try it yourself
Change, predict, then run
Write a StackTest whose @BeforeEach assigns a fresh ArrayDeque<String> to a field, add one @Test that pushes 'a' and asserts pop() returns 'a', and add another @Test that uses assertThrows to check pop() on the untouched deque throws NoSuchElementException.
Open the Java workspaceCheck your understanding
Two @Test methods in one class each add a single element to a non-static List field that is initialized inline, then assert the list has size 1. Both pass. What makes that work?
- JUnit clears non-static fields between test methods before invoking the next one.
- JUnit constructs a new instance of the test class for each test method, so the field initializer runs again.
- An inline field initializer is compiled into an implicit @BeforeEach callback.
- They pass only because the two methods happen to run in declaration order.
Show answer
Under the default Lifecycle.PER_METHOD the engine instantiates the class once per @Test, so the initializer creates a new list for each one. The 'clears fields' option is tempting but JUnit never reaches into your object to reset anything, which is precisely why changing the field to static would carry the first element into the second test and break it.