JUnit 5 Annotations — Complete Reference
JUnit 5 provides rich annotations to control test lifecycle, naming, ordering, and execution. This chapter covers every annotation you’ll use daily.
Lifecycle Annotations
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void initAll() {
System.out.println("Once before ALL tests (static)");
// Database connection, test container startup, etc.
}
@BeforeEach
void init() {
System.out.println("Before EACH test");
// Reset state, create fresh objects
}
@Test
void testOne() {
System.out.println("Test 1");
}
@Test
void testTwo() {
System.out.println("Test 2");
}
@AfterEach
void tearDown() {
System.out.println("After EACH test");
// Cleanup per-test resources
}
@AfterAll
static void tearDownAll() {
System.out.println("Once after ALL tests (static)");
// Close connections, stop containers
}
}
Output:
Once before ALL tests
Before EACH test → Test 1 → After EACH test
Before EACH test → Test 2 → After EACH test
Once after ALL tests
| Annotation | Runs | Must Be Static? |
|---|---|---|
@BeforeAll |
Once before all tests | Yes* |
@BeforeEach |
Before each test method | No |
@AfterEach |
After each test method | No |
@AfterAll |
Once after all tests | Yes* |
*Unless using
@TestInstance(Lifecycle.PER_CLASS).
@DisplayName — Human-Readable Test Names
@DisplayName("User Registration Service")
class UserRegistrationTest {
@Test
@DisplayName("should create user when email is valid")
void shouldCreateUserWhenEmailIsValid() {
// ...
}
@Test
@DisplayName("should throw exception when email is blank")
void shouldThrowWhenEmailBlank() {
// ...
}
}
IDE output:
User Registration Service
✓ should create user when email is valid
✓ should throw exception when email is blank
@Disabled — Skip Tests
@Test
@Disabled("Bug #1234 — waiting for API fix")
void shouldProcessPayment() {
// This test won't run
}
@Disabled("Migration in progress")
class LegacyServiceTest {
// Entire class skipped
}
Conditional Disabling
@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() {}
@Test
@EnabledOnJre(JRE.JAVA_21)
void onlyOnJava21() {}
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void onlyInCI() {}
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = "aarch64")
void onlyOnArm() {}
@Tag — Categorize Tests
@Tag("fast")
@Test
void shouldValidateEmail() {}
@Tag("slow")
@Tag("integration")
@Test
void shouldQueryDatabase() {}
Run only tagged tests with Maven:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups>fast</groups> <!-- include -->
<excludedGroups>slow</excludedGroups> <!-- exclude -->
</configuration>
</plugin>
mvn test -Dgroups=fast
@Timeout — Fail Slow Tests
@Test
@Timeout(5) // fail if takes longer than 5 seconds
void shouldRespondQuickly() {
// ...
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void shouldBeVeryFast() {
// ...
}
Class-Level Timeout
@Timeout(10)
class PerformanceTest {
// All tests in this class must complete within 10 seconds each
}
@Order and @TestMethodOrder
By default, test execution order is not guaranteed. To enforce order:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {
@Test
@Order(1)
void createUser() {
System.out.println("Step 1: Create user");
}
@Test
@Order(2)
void verifyUser() {
System.out.println("Step 2: Verify user");
}
@Test
@Order(3)
void deleteUser() {
System.out.println("Step 3: Delete user");
}
}
Other Orderers
@TestMethodOrder(MethodOrderer.DisplayName.class) // alphabetical by display name
@TestMethodOrder(MethodOrderer.MethodName.class) // alphabetical by method name
@TestMethodOrder(MethodOrderer.Random.class) // random (good for finding dependencies)
Warning: Ordered tests often indicate a design smell. Tests should be independent. Use ordering only for integration test scenarios.
@TestInstance — Lifecycle Per Class
By default, JUnit creates a new instance per test method. You can change this:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedStateTest {
private final List<String> items = new ArrayList<>();
@BeforeAll
void setUp() { // no longer needs to be static!
items.add("initial");
}
@Test
void testA() {
items.add("A");
assertEquals(2, items.size());
}
@Test
void testB() {
// ⚠️ items may contain "A" if testA ran first!
// This is why PER_METHOD (default) is safer
}
}
Use
PER_CLASSwhen@BeforeAll/@AfterAllneed instance methods (non-static), or with@Testcontainers.
Annotation Summary
| Annotation | Purpose |
|---|---|
@Test |
Marks a test method |
@BeforeEach / @AfterEach |
Per-test setup/teardown |
@BeforeAll / @AfterAll |
Per-class setup/teardown |
@DisplayName |
Human-readable name |
@Disabled |
Skip test/class |
@Tag |
Categorize (fast, slow, integration) |
@Timeout |
Fail if too slow |
@Order |
Execution order |
@TestMethodOrder |
Ordering strategy |
@TestInstance |
Lifecycle: per-method vs per-class |
@RepeatedTest |
Run N times |
@ParameterizedTest |
Run with different inputs |
Common Mistakes
| Mistake | Fix |
|---|---|
@BeforeAll method not static |
Make it static or use @TestInstance(PER_CLASS) |
| Relying on test order | Tests should be independent — use @BeforeEach for setup |
@Disabled without reason |
Always add a reason string |
@Tag with spaces |
Use simple strings: "fast", "integration" |
Key Takeaways
- Use
@BeforeEachfor per-test setup,@BeforeAllfor expensive one-time setup. @DisplayNamemakes test output readable for non-developers.@Taglets you run subsets of tests (fast in dev, all in CI).@Timeoutcatches performance regressions.- Tests should be independent — avoid relying on
@Order.
What’s Next?
Next: Assertions in JUnit 5 — assertEquals, assertThrows, assertAll, and fluent assertions with AssertJ.