Testing with JUnit 5 & Mockito

Test Lifecycle & Setup

15 min Lesson 4 of 13

Test Lifecycle & Setup

Well-structured tests share a predictable rhythm: arrange resources before a test runs, exercise the subject under test, then clean up afterward. JUnit 5 formalises this rhythm with four lifecycle annotations: @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll. Understanding exactly when each fires — and why — is the difference between a test suite that is fast, isolated, and maintainable, and one that is brittle and full of hidden state.

The Execution Order at a Glance

JUnit 5 executes these callbacks in a strict order for each test class:

  1. @BeforeAll — runs once before the first test method in the class.
  2. @BeforeEach — runs before every test method.
  3. Test method executes.
  4. @AfterEach — runs after every test method.
  5. @AfterAll — runs once after the last test method in the class.
Test instance model: By default JUnit 5 creates a new instance of the test class for each test method. This is intentional — it prevents tests from leaking state through instance fields. @BeforeAll and @AfterAll must therefore be static (they belong to the class, not to an instance) unless you switch to @TestInstance(Lifecycle.PER_CLASS).

@BeforeEach — Per-Test Preparation

@BeforeEach is the workhorse of test setup. Use it to build a fresh, known-good state for each test so that no test can be influenced by a previous one. Typical uses include constructing the system under test, seeding a small in-memory data structure, or opening a resource that needs to be closed after the test.

import org.junit.jupiter.api.*; import java.util.*; class ShoppingCartTest { private ShoppingCart cart; // instance field — safe because JUnit creates // a new ShoppingCartTest per test method @BeforeEach void setUp() { cart = new ShoppingCart(); // always starts empty; no shared state cart.addItem("Widget", 9.99); } @Test void totalReflectsAddedItems() { cart.addItem("Gadget", 4.99); Assertions.assertEquals(14.98, cart.total(), 0.001); } @Test void removeItemReducesTotal() { cart.removeItem("Widget"); Assertions.assertEquals(0.0, cart.total(), 0.001); } }

Because setUp() runs before each test, both methods start with a cart that already contains one Widget. Neither test can break the other by leaving behind stale cart state.

@AfterEach — Per-Test Teardown

@AfterEach mirrors @BeforeEach. Use it to release resources that were acquired in @BeforeEach — closing a file handle, resetting a static mock, or rolling back a database transaction. JUnit guarantees @AfterEach runs even when the test itself throws an exception, which makes it the right place for cleanup that must always happen.

class FileProcessorTest { private Path tempFile; @BeforeEach void createTempFile() throws Exception { tempFile = Files.createTempFile("junit-", ".txt"); Files.writeString(tempFile, "hello"); } @AfterEach void deleteTempFile() throws Exception { Files.deleteIfExists(tempFile); // runs even if the test fails } @Test void readsFileContent() throws Exception { String content = new FileProcessor().read(tempFile); Assertions.assertEquals("hello", content); } }
Prefer AutoCloseable fixtures. When a resource implements AutoCloseable, consider JUnit 5's @TempDir extension (for directories) or the CloseableResource Store API instead of manual @AfterEach cleanup. They are less error-prone because the framework handles closing automatically.

@BeforeAll — One-Time Class Setup

Some fixtures are expensive to create and safe to share across tests: a database connection pool, a started server, or a compiled regex. @BeforeAll runs once per class, making it ideal for this kind of costly, read-only shared state.

@TestInstance(TestInstance.Lifecycle.PER_CLASS) // allows non-static @BeforeAll class DatabaseIntegrationTest { private DataSource dataSource; @BeforeAll void startDatabase() { // Testcontainers, H2, or any embedded DB — started once for the whole class dataSource = EmbeddedDatabase.start(); } @AfterAll void stopDatabase() { dataSource.close(); } @BeforeEach void seedData(TestInfo info) { // insert fresh rows before each test — cheap because the DB is already running try (var conn = dataSource.getConnection()) { conn.createStatement().execute("DELETE FROM orders"); conn.createStatement().execute("INSERT INTO orders VALUES (1, 'OPEN')"); } } @Test void findsOpenOrder() { var repo = new OrderRepository(dataSource); Assertions.assertEquals("OPEN", repo.findById(1).status()); } }

Notice the pattern: @BeforeAll starts the database once; @BeforeEach resets data before each test. The expensive operation happens once; isolation is still maintained cheaply.

Mutable shared state is a trap. If tests write to a @BeforeAll fixture and do not reset it, later tests can fail because of earlier ones — a classic order-dependent test failure. Reserve @BeforeAll for resources that are either truly read-only from the test's perspective (a compiled regex, a connection pool) or are reset in @BeforeEach.

Static vs. @TestInstance(PER_CLASS)

With the default PER_METHOD lifecycle, @BeforeAll and @AfterAll methods must be static because no instance exists when they are called. Annotating the class with @TestInstance(TestInstance.Lifecycle.PER_CLASS) tells JUnit to create one shared instance for the whole class, removing the static requirement — handy when the fixture itself is not statically obtainable (e.g. an injected Spring ApplicationContext).

// Default PER_METHOD — @BeforeAll must be static class DefaultLifecycleTest { @BeforeAll static void globalSetup() { System.out.println("runs once, statically"); } } // PER_CLASS — @BeforeAll can be an instance method @TestInstance(TestInstance.Lifecycle.PER_CLASS) class PerClassLifecycleTest { private final List<String> log = new ArrayList<>(); @BeforeAll void globalSetup() { // not static log.add("BeforeAll"); } @Test void logIsNotEmpty() { Assertions.assertFalse(log.isEmpty()); } }

Naming and Ordering Best Practices

  • Name @BeforeEach methods setUp() or something descriptive like givenFreshCart(). A descriptive name makes failing setup easier to diagnose.
  • Keep @BeforeEach short. If setup exceeds 10–15 lines it is a signal the test class is doing too much — split it.
  • Use @DisplayName on the test class and methods to write human-readable names that appear in reports without cryptic method identifiers.
  • Avoid putting assertions inside @BeforeEach; setup failures already surface as errors, and assertions add noise.
  • If multiple @BeforeEach methods exist (possible via inheritance), JUnit calls the superclass method first. Rely on this to layer shared base-class setup with per-subclass refinements.

Inheritance and Shared Fixtures

A common pattern in large codebases is a base test class that handles boilerplate — starting a web client, configuring logging — and concrete subclasses that add domain-specific setup:

abstract class BaseWebTest { protected HttpClient client; @BeforeEach void initClient() { client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); } } class UserApiTest extends BaseWebTest { private UserService service; @BeforeEach // runs AFTER the superclass @BeforeEach void initService() { service = new UserService(/* deps */); } @Test void createUser() { // client is already initialised by the parent setUp Assertions.assertNotNull(client); Assertions.assertNotNull(service); } }

Summary

@BeforeEach and @AfterEach are your primary tools for test isolation — they guarantee a clean slate before every test and reliable cleanup afterward. @BeforeAll and @AfterAll optimise expensive, inherently shared fixtures that would make the suite slow if repeated per test. The golden rule is to share state only when it is truly read-only or reset in @BeforeEach. This discipline keeps your test suite fast, deterministic, and trustworthy — the foundations you will rely on in every subsequent lesson on Mockito, TDD, and best practices.