Appearance
Test Lifecycle, @TestInstance and Parallel Execution
With the architecture in place, this chapter writes tests. It covers, in order: the dependencies a project needs, the lifecycle methods that run around tests (@BeforeAll, @BeforeEach, @AfterEach, @AfterAll), how many test-class instances JUnit creates (@TestInstance), running tests in parallel, and the AAA pattern that every test follows.
Dependencies
In a Spring Boot project, spring-boot-starter-test brings in everything needed to run JUnit 5 tests: org.junit.jupiter:junit-jupiter (the Jupiter engine and API) and org.junit.platform:junit-platform-launcher.
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>In a plain Maven project, add org.junit.jupiter:junit-jupiter with test scope.
The lifecycle annotations
| Annotation | Runs | Typical use |
|---|---|---|
@BeforeAll | once, before all tests of the class | expensive setup shared by all tests, such as a database connection |
@BeforeEach | before each test | fresh test data: new objects, temporary rows in a DB |
@Test | the test itself | |
@AfterEach | after each test | cleanup: close files, delete temporary data |
@AfterAll | once, after all tests | release shared resources, such as closing the DB connection |
Two lifecycles
Lifecycle.PER_METHOD (the default)
A new instance of the test class is created for each test method.
@BeforeAll runs once, before any instance exists, so it must be static; so must @AfterAll. A non-static @BeforeAll makes test discovery fail: "@BeforeAll method ... must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS)".
java
public class Junit5LifecycleDemo {
public Junit5LifecycleDemo() {
System.out.println("Junit5LifecycleDemo class Instance created");
}
@BeforeAll
static void beforeAll() {
System.out.println("inside @BeforeAll method");
}
@BeforeEach
void beforeEach() {
System.out.println("inside @BeforeEach method");
}
@Test
void testcase1() {
System.out.println("inside Testcase 1");
}
@Test
void testcase2() {
System.out.println("inside Testcase 2");
}
@AfterEach
void afterEach() {
System.out.println("inside @AfterEach method");
}
@AfterAll
static void afterAll() {
System.out.println("inside @AfterAll method");
}
}text
inside @BeforeAll method
Junit5LifecycleDemo class Instance created
inside @BeforeEach method
inside Testcase 1
inside @AfterEach method
Junit5LifecycleDemo class Instance created
inside @BeforeEach method
inside Testcase 2
inside @AfterEach method
inside @AfterAll methodLifecycle.PER_CLASS
One instance of the test class for all its tests:
java
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class Junit5LifecycleDemo {
// same methods, but @BeforeAll and @AfterAll may now be non-static
}text
Junit5LifecycleDemo class Instance created
inside @BeforeAll method
inside @BeforeEach method
inside Testcase 1
inside @AfterEach method
inside @BeforeEach method
inside Testcase 2
inside @AfterEach method
inside @AfterAll methodThe instance is created first, and only once; since it exists before @BeforeAll, those methods can be non-static.
Which to use
PER_METHOD is preferred:
- Test isolation: every test starts from a fresh object, so tests can run in any order. That is what "testing in isolation" means.
- Easy debugging: a failure cannot be caused by state left behind by another test.
- Safe for parallel execution.
Use PER_CLASS only when tests must share state, and then make sure the test code is thread-safe, since parallel tests would share the one instance.
Several methods with the same annotation
A class may have several @BeforeAll, @BeforeEach, @AfterEach or @AfterAll methods. They all run, but the order between them is not guaranteed (JUnit uses a deterministic but deliberately non-obvious order). Do not rely on it.
Parallel execution
By default tests run sequentially, one after another, on the main thread. With PER_METHOD's isolation, they can safely run in parallel.
Who runs them in parallel? Not the launcher: the launcher only calls each engine's execute. The engine decides how to run its tests; Jupiter can run them concurrently on a thread pool, which is why the settings are junit.jupiter.* and apply to Jupiter tests only.
java
public class Junit5LifecycleDemo {
@Test
void testcaseA() throws InterruptedException {
System.out.println("inside testcaseA, executed by: " + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("Finished testcaseA");
}
@Test
void testcaseB() throws InterruptedException {
System.out.println("inside testcaseB, executed by: " + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("Finished testcaseB");
}
}Create src/test/resources/junit-platform.properties (exactly this name; JUnit reads it):
properties
# enable parallel execution (default: false)
junit.jupiter.execution.parallel.enabled = true
# methods within a test class run in parallel (default: same_thread, i.e. sequentially)
junit.jupiter.execution.parallel.mode.default = concurrent
# test classes run in parallel with each other (default: same_thread)
junit.jupiter.execution.parallel.mode.classes.default = concurrent
# thread pool strategy:
# dynamic (default): pool size computed from the available processors
# fixed: you choose the number of threads
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 5Sequential:
text
inside testcaseA, executed by: main
Finished testcaseA
inside testcaseB, executed by: main
Finished testcaseBParallel:
text
inside testcaseA, executed by: ForkJoinPool-1-worker-2
inside testcaseB, executed by: ForkJoinPool-1-worker-3
Finished testcaseB
Finished testcaseABoth start at once on different worker threads, each with its own instance and its own @BeforeEach/@AfterEach.
The AAA pattern
Every test follows Arrange, Act, Assert:
java
@Test
void testIsAdult() {
// Arrange: set up objects and data (and mocks)
User user = new User("XYZ", 20);
// Act: call the method under test
boolean result = user.isAdult();
// Assert: check the result against the expectation
assertTrue(result);
}- Arrange: create the objects, test data and mocks.
- Act: invoke the method being tested.
- Assert: check that the result matches the expectation. If a bug made
isAdult()returnfalsefor age 20, this test fails.
Next: The last step of AAA is assert. Chapter 5 covers JUnit 5's assertions in depth, including how to test that code throws the right exception.
Check yourself
- In what order do
@BeforeAll, the constructor,@BeforeEach, the test and@AfterEachrun with the default lifecycle? - Why must
@BeforeAllbe static by default, and when can it be non-static? - How many instances are created for a class with 10 tests under
PER_METHOD? UnderPER_CLASS? - Why is
PER_METHODpreferred? - When would you choose
PER_CLASS, and what must you then take care of? - Two methods are annotated
@BeforeEach. Which runs first? - Which component actually runs Jupiter tests in parallel, and where do you enable it?
- What is the difference between
parallel.mode.defaultandparallel.mode.classes.default? - What do the three A's of AAA stand for?
Answers
@BeforeAll, then for each test: constructor,@BeforeEach, the test,@AfterEach; finally@AfterAll.- It runs before any instance exists. With
@TestInstance(Lifecycle.PER_CLASS). - Ten. One.
- Each test gets a fresh instance: isolation, any order, easier debugging, safe in parallel.
- When tests must share state; the test code must then be thread-safe.
- Not guaranteed; do not depend on it.
- The Jupiter engine; in
src/test/resources/junit-platform.propertieswithjunit.jupiter.execution.parallel.enabled=trueand the mode settings. - The first runs the methods within a class in parallel; the second runs different classes in parallel.
- Arrange, Act, Assert.