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:
@BeforeAll
– Runs once before any test methods.@BeforeEach
– Runs before each test method.@Test
– Executes the test logic.@AfterEach
– Runs after each test method.@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 bepublic
or package-private.- Tests should include assertions like
assertEquals
,assertTrue
, orassertThrows
.
@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.