JUnit 5 Annotations Explained: @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll

Illustration for JUnit 5 Annotations Explained: @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll
By Last updated:

Every Java developer knows that testing is critical, but understanding the lifecycle of tests can be tricky without the right knowledge. JUnit 5 provides a set of annotations that help structure test execution, manage setup and teardown, and ensure tests run predictably in both local development and CI/CD pipelines.

In this tutorial, we’ll break down the core JUnit 5 annotations — @Test, @BeforeEach, @AfterEach, @BeforeAll, and @AfterAll. We’ll explore how they work, where to use them, and best practices for writing clean, maintainable tests.


Why Annotations Matter in Testing

  • Consistency: Ensure setup and cleanup logic runs at the right time.
  • Maintainability: Reduce code duplication with common setup methods.
  • CI/CD Readiness: Stable, repeatable tests that fit into automated pipelines.
  • Scalability: Clear test structure helps large teams collaborate effectively.

The JUnit 5 Test Lifecycle

When you run a test class in JUnit 5, the following order is applied:

  1. @BeforeAll – Runs once before any test methods.
  2. @BeforeEach – Runs before each test method.
  3. @Test – Executes the test logic.
  4. @AfterEach – Runs after each test method.
  5. @AfterAll – Runs once after all test methods complete.

@Test — Defining a Test Method

The most fundamental annotation in JUnit 5 is @Test. It marks a method as a test case.

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

class CalculatorTest {

    @Test
    void testAddition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result, "2 + 3 should equal 5");
    }
}
  • @Test methods must be public or package-private.
  • Tests should include assertions like assertEquals, assertTrue, or assertThrows.

@BeforeEach — Setup Before Each Test

Use @BeforeEach to execute logic before every test method. Ideal for resetting state or initializing objects.

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

class UserServiceTest {

    private UserService userService;

    @BeforeEach
    void setup() {
        userService = new UserService();
    }

    @Test
    void testCreateUser() {
        User user = userService.createUser("Alice");
        assert user.getName().equals("Alice");
    }
}

@AfterEach — Cleanup After Each Test

@AfterEach runs after each test, perfect for releasing resources like file handles, streams, or database connections.

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

class FileHandlerTest {

    private FileHandler handler;

    @Test
    void testWriteFile() {
        handler = new FileHandler("test.txt");
        handler.write("Hello");
    }

    @AfterEach
    void cleanup() {
        handler.close();
        System.out.println("Resources released");
    }
}

@BeforeAll — Setup Before All Tests

@BeforeAll runs once before all test methods. Commonly used for expensive operations like starting a database container.
Methods annotated with @BeforeAll must be static unless you use @TestInstance(Lifecycle.PER_CLASS).

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

class DatabaseTest {

    @BeforeAll
    static void globalSetup() {
        System.out.println("Connecting to database...");
    }

    @Test
    void testConnection() {
        System.out.println("Running database test");
    }
}

@AfterAll — Teardown After All Tests

@AfterAll runs once after all tests finish, typically used for shutting down shared resources.

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

class ServerTest {

    @AfterAll
    static void globalTeardown() {
        System.out.println("Shutting down server...");
    }

    @Test
    void testServerRunning() {
        System.out.println("Server is running test");
    }
}

Combining Lifecycle Annotations

JUnit 5 annotations work best together to create predictable test flows.

import org.junit.jupiter.api.*;

class LifecycleDemoTest {

    @BeforeAll
    static void beforeAll() { System.out.println("Before all tests"); }

    @BeforeEach
    void beforeEach() { System.out.println("Before each test"); }

    @Test
    void testOne() { System.out.println("Running test one"); }

    @Test
    void testTwo() { System.out.println("Running test two"); }

    @AfterEach
    void afterEach() { System.out.println("After each test"); }

    @AfterAll
    static void afterAll() { System.out.println("After all tests"); }
}

Execution Output:

Before all tests
Before each test
Running test one
After each test
Before each test
Running test two
After each test
After all tests

Best Practices for Using JUnit 5 Annotations

  • Use @BeforeEach to keep tests independent.
  • Avoid heavy setup in @BeforeAll unless necessary.
  • Ensure @AfterEach and @AfterAll release resources properly.
  • Name test methods descriptively (shouldSaveUserCorrectly).
  • Combine with Mockito for mocking dependencies.
  • Use Testcontainers for integration testing with real services.

Version Tracker

  • JUnit 4 → JUnit 5: @Before@BeforeEach, @After@AfterEach, @BeforeClass@BeforeAll, @AfterClass@AfterAll.
  • Mockito Updates: Support for static and final mocking improves test flexibility.
  • Testcontainers Ecosystem: Expanded modules for databases, message brokers, and cloud-native testing.

Conclusion & Key Takeaways

Mastering JUnit 5 annotations is essential for writing structured, maintainable, and scalable tests. These lifecycle hooks — from @BeforeEach to @AfterAll — give you full control over test execution, ensuring consistency and reliability.

Key Takeaways:

  • @Test defines the test.
  • @BeforeEach / @AfterEach manage per-test setup and cleanup.
  • @BeforeAll / @AfterAll handle global resources.
  • Use annotations strategically for efficient, reliable test suites.

FAQ

1. What is the difference between @BeforeEach and @BeforeAll?
@BeforeEach runs before every test, while @BeforeAll runs only once before all tests.

2. Do @BeforeAll and @AfterAll methods need to be static?
Yes, unless you use @TestInstance(Lifecycle.PER_CLASS).

3. Can I disable a test temporarily?
Yes, use @Disabled with an optional reason.

4. How do I integrate JUnit 5 with Mockito?
Use @ExtendWith(MockitoExtension.class) for injecting mocks.

5. What happens if a @BeforeEach method fails?
The corresponding test is skipped, and execution moves to the next test.

6. Can I use multiple @BeforeEach methods?
Yes, they are executed in the order they appear in the class.

7. How do I share expensive resources across tests?
Initialize them in @BeforeAll and release in @AfterAll.

8. Can I use annotations with parameterized tests?
Yes, lifecycle annotations apply to parameterized tests as well.

9. How are annotations handled in nested test classes?
Each nested class has its own lifecycle, respecting parent setups.

10. Should I always use @AfterEach for cleanup?
Yes, it ensures resources are consistently released after every test.