Skip to main content

JUnit @BeforeEach and @AfterEach: The Complete Test Lifecycle Guide

Everything you need on JUnit’s per-test lifecycle: @BeforeEach and @AfterEach rules, real setup/teardown patterns (databases, mocks, security context, temp files), the PER_METHOD lifecycle diagram, inheritance order, and a JUnit 6 compatibility note.

Every JUnit test needs two things around it: a clean starting state, and a guarantee that whatever the test touched gets cleaned up afterward. @BeforeEach and @AfterEach are the two annotations that give you both. Used correctly, they turn a test suite where order matters and tests fail mysteriously into one where every test runs in total isolation, pass or fail. This guide covers both annotations together — what they do, the rules the JUnit Jupiter engine enforces on them, real setup/teardown patterns from database connections to security contexts, and the inheritance and instance-lifecycle rules that trip people up once tests get more complex than a single flat class.

Versions and JUnit 6. Everything below applies unchanged whether you’re on JUnit Jupiter 5.11.0 (still receiving maintenance releases, with 5.14.x as the current LTS line for projects that can’t move off Java 8-11) or the newer JUnit 6 line, currently at 6.0.3. JUnit 6’s own release notes describe the annotation model — @BeforeEach, @AfterEach, and the rest — as untouched; the breaking changes in 6.0 are elsewhere (removed legacy modules, a stricter CSV parser, a Java 17 baseline instead of JUnit 5’s Java 8). If you want the full four-annotation picture including @BeforeAll/@AfterAll and the PER_CLASS instance lifecycle under JUnit 6 specifically, see the JUnit 6 Test Lifecycle deep dive linked at the end of this post.


What is @BeforeEach?

The @BeforeEach annotation marks a method that must run before every individual @Test method in the current class. Its primary goal is to ensure test isolation. By resetting the state before every test, you guarantee that “Test A” cannot leave behind data that causes “Test B” to pass or fail incorrectly.

The Evolution: From JUnit 4 to JUnit 5 and 6

If you are migrating legacy code, remember that @BeforeEach is the direct successor to JUnit 4’s @Before. While the name changed in JUnit 5 to be more descriptive, the core concept remains the same across every version since: it’s your primary lifecycle hook for per-test initialization, and that hasn’t changed again in JUnit 6.

Core Rules & Requirements

To ensure the JUnit Jupiter engine can discover and execute your setup methods, you must adhere to the following technical constraints:

  1. Return Type: The method must return void. It is a side-effect method intended for state preparation.
  2. Visibility: Methods must not be private. While public works, the idiomatic JUnit 5 style is to use the default package-private visibility.
  3. Static vs. Instance: Methods must not be static. Because @BeforeEach is tied to the instance of the test class created for each test, it requires an instance context. (Static setup is reserved for @BeforeAll).
  4. Exceptions: These methods can throw checked or unchecked exceptions. Note that if a @BeforeEach method fails, JUnit will mark the subsequent @Test as “Aborted” or “Failed” without even attempting to run the test logic.

Hands-On Example: The “Fresh Start” Pattern

Imagine we are testing a ShoppingCart service. To ensure one test doesn’t accidentally check out items added by a previous test, we need a clean slate every time.

1. The Class to Test

public class ShoppingCart {
    private List<String> items = new ArrayList<>();
    
    public void addItem(String item) { items.add(item); }
    public int getSize() { return items.size(); }
    public void clear() { items.clear(); }
}

2. Implementation with @BeforeEach

package com.ankurm.testing;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.assertEquals;

class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void init() {
        // This runs before EACH @Test method
        System.out.println("DEBUG: Setting up a fresh shopping cart...");
        cart = new ShoppingCart();
    }

    @Test
    @DisplayName("Should increment size when item is added")
    void testAddItem() {
        System.out.println("DEBUG: Running testAddItem");
        cart.addItem("Laptop");
        assertEquals(1, cart.getSize());
    }

    @Test
    @DisplayName("New cart should have zero items")
    void testCartIsInitiallyEmpty() {
        System.out.println("DEBUG: Running testCartIsInitiallyEmpty");
        assertEquals(0, cart.getSize());
    }
}

Console Output:

DEBUG: Setting up a fresh shopping cart...
DEBUG: Running testAddItem
DEBUG: Setting up a fresh shopping cart...
DEBUG: Running testCartIsInitiallyEmpty

Real-World @BeforeEach Scenarios

In professional enterprise environments, @BeforeEach is used for more than just simple object instantiation. Here are 4 high-impact real-world examples.

1. Database State Initialization

When testing Data Access Objects (DAOs), you need to ensure the database starts with specific seed data for every test.

@BeforeEach
void initDatabase() {
    System.out.println("LOG: Cleaning tables and seeding User data...");
    database.execute("DELETE FROM users");
    database.execute("INSERT INTO users (id, name) VALUES (1, 'Ankur')");
}

Output:

LOG: Cleaning tables and seeding User data...
Executing Test: findUserById()
LOG: Cleaning tables and seeding User data...
Executing Test: deleteUser()

2. Mocking REST API Responses with Mockito

If your service depends on a third-party API (like Stripe or Twilio), you use @BeforeEach to define a “default” successful behavior for your mocks.

@Mock
private ExternalApiClient apiClient;

@BeforeEach
void mockExternalApi() {
    System.out.println("LOG: Setting default API mock behavior...");
    when(apiClient.checkStatus()).thenReturn("ACTIVE");
}

Output:

LOG: Setting default API mock behavior...
Running: testServiceWithActiveApi()

3. Setting Up Security/Authentication Context

In Spring Security or similar frameworks, you often need to mock a “Logged In” user before the test logic executes.

@BeforeEach
void authenticateUser() {
    System.out.println("LOG: Injecting 'ADMIN' security context...");
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken("admin", "password", List.of(new SimpleGrantedAuthority("ROLE_ADMIN")))
    );
}

Output:

LOG: Injecting 'ADMIN' security context...
Running: testAdminSensitiveOperation()

4. Temporary File and IO Preparation

When testing File IO logic, you need to ensure a specific directory structure exists before the test starts writing data.

private Path tempDir;

@BeforeEach
void prepareFileSystem() throws IOException {
    tempDir = Files.createTempDirectory("junit_test_");
    System.out.println("LOG: Created temporary directory: " + tempDir.getFileName());
}

Output:

LOG: Created temporary directory: junit_test_837264
Running: testFileWriteOperation()

Advanced Usage: Dependency Injection

JUnit 5 allows you to pass parameters into your @BeforeEach methods. The most common parameters are TestInfo (for metadata) and TestReporter (for logging).

@BeforeEach
void setup(TestInfo testInfo) {
    String displayName = testInfo.getDisplayName();
    System.out.println("INFO: Preparing environment for: " + displayName);
    this.cart = new ShoppingCart();
}


Deep Dive: @BeforeEach Inheritance and Ordering

In complex test suites, you often leverage inheritance to avoid duplicating common setup logic across multiple test classes. JUnit 5 handles this hierarchy with a specific, deterministic execution order that ensures the environment is constructed from the ground up.

The Rule of Inheritance:

  1. Parent @BeforeEach methods run first: The “Base” or superclass methods are executed to initialize global infrastructure or shared resources.
  2. Child @BeforeEach methods run second: The specific test class (the subclass) then runs its own setup logic, allowing it to build upon or specialize the environment provided by the parent.

This “Outside-In” approach is critical because child classes often depend on variables or connections initialized in the parent. By executing parent methods first, JUnit 5 prevents NullPointerException errors that would occur if a subclass tried to use a resource that hadn’t been created yet.

abstract class BaseTest {
    @BeforeEach
    void baseSetup() { 
        System.out.println("BASE: Global Infrastructure Setup (e.g., Container Start)"); 
    }
}

class SpecificTest extends BaseTest {
    @BeforeEach
    void specificSetup() { 
        System.out.println("CHILD: Local Test Data Seeding (e.g., User Mocking)"); 
    }

    @Test
    void myTest() { 
        System.out.println("TEST: Execution of Business Logic Checks"); 
    }
}

Output:

BASE: Global Infrastructure Setup (e.g., Container Start)
CHILD: Local Test Data Seeding (e.g., User Mocking)
TEST: Execution of Business Logic Checks

This sequence is repeated for every test method in the child class. If you have a deep inheritance tree (e.g., Grandparent -> Parent -> Child), the order remains consistent: Top-level superclass down to the most specific subclass. This predictable flow allows developers to design robust, modular testing architectures where each layer of the hierarchy has a distinct, well-defined responsibility.


What is @AfterEach?

The @AfterEach annotation marks a method that should be executed after every individual @Test method in the current class. Think of it as the “janitor” of your testing suite; no matter if your test passes, fails, or throws an unexpected exception, the @AfterEach method steps in to sweep the floor and reset the stage for the next performer.

Unlike its counterpart @AfterAll (which runs once after the entire class is finished), @AfterEach is granular. It ensures that the state is reset immediately after each logic check, preventing “leakage” where the side effects of Test A interfere with the assertions of Test B.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

public class CleanupExample {
    
    @AfterEach
    void tearDown() {
        // This runs after EVERY test method to ensure a clean slate
        System.out.println("Cleaning up resources and resetting state...");
    }
    
    @Test
    void firstTest() {
        System.out.println("Executing first test: Checking User creation");
    }
    
    @Test
    void secondTest() {
        System.out.println("Executing second test: Checking User deletion");
    }
}

Output:

Executing first test: Checking User creation
Cleaning up resources and resetting state...
Executing second test: Checking User deletion
Cleaning up resources and resetting state...

The Per-Method Lifecycle, Visually

To use @BeforeEach and @AfterEach effectively, you have to understand the “sandwich” structure of a JUnit test execution. By default (the PER_METHOD test instance lifecycle), JUnit creates a brand-new instance of your test class for every single @Test method — that’s what makes per-test isolation the default rather than something you have to opt into. Here’s that cycle for a class with two @Test methods:

new TestClass()  ──▶ @BeforeEach ──▶ @Test #1 ──▶ @AfterEach ──▶ (instance discarded)
new TestClass()  ──▶ @BeforeEach ──▶ @Test #2 ──▶ @AfterEach ──▶ (instance discarded)
                        ...repeats for every @Test method in the class...

Per @Test method:
  1. Instantiate the test class (a fresh instance every time)
  2. Run every @BeforeEach method   (Initialization)
  3. Run the one @Test method       (Execution)
  4. Run every @AfterEach method    (Teardown  -  runs even if step 3 failed)

Two things about that diagram catch people out. First, because a new instance backs every test, instance fields you set in one test are simply gone by the time the next one starts — there’s no shared state to accidentally leak unless you make something static. Second, @AfterEach is not conditional on the test passing: step 4 runs whether step 3 threw, failed an assertion, or completed cleanly, which is exactly what makes it the right place for cleanup and the wrong place to assume “the object I’m closing definitely got created.” (JUnit 6 keeps this exact sequence; the only lifecycle-adjacent thing it adds is that with @TestInstance(Lifecycle.PER_CLASS), one instance is reused for the whole class instead of one per test — see the JUnit 6 Test Lifecycle guide for that variant in full.)


Why Use @AfterEach?

  • Strict Isolation: It guarantees that the outcome of one test doesn’t influence the next. In a truly unit-tested environment, the order of test execution should never matter.
  • Resource Management: It prevents memory leaks and resource exhaustion by closing streams, network sockets, or database connection pools that were opened during the setup.
  • Reliability & Resilience: It provides a safety net. Even if a test crashes halfway through, JUnit 5 guarantees that the teardown logic will be triggered.
  • State Reset: It allows you to clear “static” or shared state, such as clearing a singleton’s internal registry or resetting System properties.

Common Use Cases for @AfterEach

In professional enterprise environments, you’ll most frequently use this annotation for:

  • Closing Database Connections: Releasing connections back to the pool to avoid “too many connections” errors.
  • Deleting Temporary Files: Cleaning up the /tmp folder or local IO buffers to prevent disk bloat.
  • Resetting Mocks: Using Mockito.reset(mock) to ensure interaction counts don’t bleed into the next test.
  • Restoring System Properties: Reverting any changes made to System.setProperty(), which are global to the JVM.
  • Clearing Security Contexts: Ensuring no authenticated user remains in the SecurityContextHolder.

Real-World @AfterEach Scenarios

1. Database Connection Management

One of the most common pitfalls in integration testing is leaving database connections hanging. Here is how to handle it properly:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseServiceTest {
    
    private Connection connection;
    
    @BeforeEach
    void setUpConnection() throws SQLException {
        connection = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");
        System.out.println("Database connection established.");
    }
    
    @AfterEach
    void closeConnection() throws SQLException {
        if (connection != null && !connection.isClosed()) {
            connection.close();
            System.out.println("Database connection closed.");
        }
    }
    
    @Test
    void testUserInsertion() {
        System.out.println("Testing user insertion logic...");
    }
}

2. Cleaning Up Temporary Files

When testing file-based IO logic, you often create temporary files. Failing to delete them can lead to “File Already Exists” errors in subsequent tests or fill up the CI server’s disk space.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileServiceTest {
    private Path tempFile;

    @Test
    void testFileWriting() throws IOException {
        tempFile = Files.createTempFile("test_report", ".txt");
        Files.writeString(tempFile, "Hello JUnit 5");
        // Assertions...
    }

    @AfterEach
    void deleteTempFiles() throws IOException {
        if (tempFile != null) {
            Files.deleteIfExists(tempFile);
            System.out.println("Deleted temporary file: " + tempFile.getFileName());
        }
    }
}

3. Resetting System Properties

Many applications use System Properties for configuration. Since these are global to the JVM process, a change in one test can cause a failure in a completely unrelated test class.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class SystemConfigTest {
    private String originalTheme;

    @BeforeEach
    void storeOriginalState() {
        originalTheme = System.getProperty("app.ui.theme");
    }

    @AfterEach
    void restoreSystemProperties() {
        if (originalTheme != null) {
            System.setProperty("app.ui.theme", originalTheme);
        } else {
            System.clearProperty("app.ui.theme");
        }
    }

    @Test
    void testDarkThemeActivation() {
        System.setProperty("app.ui.theme", "DARK");
        // Test logic that relies on the theme being DARK
    }
}

@AfterEach Inheritance and Exception Handling

Inheritance Order Is the Reverse of @BeforeEach

If you have a base test class, JUnit 5 handles the order of execution intelligently. However, for teardown it’s the reverse of the setup order covered earlier.

  • @BeforeEach: Executes from Base → Derived.
  • @AfterEach: Executes from Derived → Base.

This ensures that the child class can clean up its specific resources before the parent class shuts down the core infrastructure.

@AfterEach Runs Even If the Test Fails

A critical feature of @AfterEach — already shown in the lifecycle diagram above — is that it runs even if the test fails.

@Test
void failingTest() {
    org.junit.jupiter.api.Assertions.fail("This test is designed to fail");
}

@AfterEach
void alwaysRuns() {
    System.out.println("Cleanup still happens, keeping the environment stable!");
}

Best Practices for @AfterEach

To write maintainable and professional-grade tests, follow these rules of thumb:

  1. Keep it Fail-Safe: Always perform null checks. If your @BeforeEach fails to initialize a resource, your @AfterEach might throw a NullPointerException when trying to close it.
  2. One Responsibility: If you have multiple cleanup tasks (e.g., DB and Files), it’s often better to have two separate @AfterEach methods. JUnit 5 supports multiple lifecycle methods.
  3. Use Descriptive Names: Instead of tearDown(), use names like clearTemporaryFolders() or resetSecurityContext().
  4. Avoid Heavy Logic: Cleanup should be fast. If your teardown takes seconds, it will drastically slow down your CI/CD pipeline.

Troubleshooting Common Pitfalls

IssueCauseSolution
Setup not runningMethod is private or static.Change to package-private and remove static.
Shared state leaksUsing static variables in setup.Use instance variables so each test gets its own reference.
Tests are slowHeavy DB/Network calls in @BeforeEach.Move heavy, one-time setup to @BeforeAll.
Teardown throws NullPointerException@BeforeEach failed before the resource was assigned, but @AfterEach still ran and tried to close it.Null-check every field in @AfterEach before calling a method on it.

Lifecycle Annotations at a Glance

@BeforeEach vs @AfterEach

Feature@BeforeEach@AfterEach
TimingRuns before each testRuns after each test
PurposeSetup / InitializationCleanup / Teardown
Inheritance OrderParent class firstChild class first
ExecutionStops if setup failsRuns even if test fails

@BeforeEach vs @BeforeAll (see the JUnit 6 Test Lifecycle guide for the full @BeforeAll/@AfterAll treatment, including PER_CLASS)

Feature@BeforeEach@BeforeAll
Execution FrequencyOnce per test methodOnce per test class
Method RequirementInstance methodStatic method (usually)
Common Use CaseResetting mocks, new objectsStarting DBs, loading Configs

Summary

@BeforeEach and @AfterEach are the two halves of the same guarantee: every test starts on a clean slate and leaves nothing behind for the next one, whether it passed or not. Centralizing setup in @BeforeEach eliminates copy-pasted initialization code, and centralizing teardown in @AfterEach eliminates the resource leaks and “flaky test” reports that come from state bleeding across tests. Both annotations, and this execution order, carry over unchanged into JUnit 6.

Next Step: Review your current tests. Are you repeating initialization logic at the start of every @Test, or manually closing resources at the end of every test body instead of in @AfterEach? If so, refactor that code into @BeforeEach/@AfterEach blocks.


Further Reading

  • JUnit 6 — the full hub: getting started, core patterns, mocking, extensions, integration testing, and best practices
  • JUnit 6 Test Lifecycle Explained — the complete four-annotation reference (@BeforeAll/@BeforeEach/@AfterEach/@AfterAll), including PER_METHOD vs PER_CLASS, nested-class composition, and inheritance FAQs in full depth

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.