JUnit 5 – When to use CSV Providers
March 10, 2024Assert with Grace: Custom Soft Assertions using AssertJ for Cleaner Code
October 13, 2024Introduction
JUnit 5 is a well-known Java Testing Framework/Library across developers. It’s the evolution of JUnit 4 and carries with it a lot of awesome features. One of the most important ones is setting pre and post-conditions as knowing by the terms Before (pre-condition) and After (post-condition).
It has 2 supported ways: Before/After All and Before/After Each.
The “All” part means that a code block can be executed as pre or post-condition before or after it can initialize all tests. The “Each” part means that a code block can be executed as a pre or post-condition before or after each test.
The JUnit 5 official docs say the following about these strategies, which are annotations:
@BeforeEach | Denotes that the annotated method should be executed before each @Test , @RepeatedTest , @ParameterizedTest , or @TestFactory method in the current class; analogous to JUnit 4’s @Before . Such methods are inherited unless they are overridden. |
@AfterEach | Denotes that the annotated method should be executed after each @Test , @RepeatedTest , @ParameterizedTest , or @TestFactory method in the current class; analogous to JUnit 4’s @After . Such methods are inherited unless they are overridden. |
@BeforeAll | Denotes that the annotated method should be executed before all @Test , @RepeatedTest , @ParameterizedTest , and @TestFactory methods in the current class; analogous to JUnit 4’s @BeforeClass . Such methods are inherited unless they are overridden and must be static unless the “per-class” test instance lifecycle is used. |
@AfterAll | Denotes that the annotated method should be executed after all @Test , @RepeatedTest , @ParameterizedTest , and @TestFactory methods in the current class; analogous to JUnit 4’s @AfterClass . Such methods are inherited unless they are overridden and must be static unless the “per-class” test instance lifecycle is used. |
What problem we are trying to solve
Only by looking at the annotations, we understand that it covers most of the scenarios we want for our tests.
We have the @BeforeAll
and @AfterAll
annotations as a general pre or post-condition.
One common testing pattern applied in good testing architectures is the BaseTest class: a place where we can add pre and post-conditions that will shared across different tests by inheritance. Normally, we want to control these conditions in different ways. Developers have different ways to control it through the BaseTest pattern:
- open the browser only once when any test starts to save time
- keep a container (Testcontainer) opened for different test classes
- ingest data before any test is executed and remove it after all are executed
I have bad news: JUnit 5 doesn’t have a way to control the three mentioned scenarios as the @BeforeAll
and @AfterAll
are executed per test instance, meaning per test class. Other frameworks like TestNG have this ability called @BeforeSuite
and @AfterSuite
, and it’s exactly what we want that JUnit 5 does not support.
Let’s understand how we could use JUnit 5 for this and how to fix this problem.
The BeforeAllCallback and AfterAllCallback interfaces
You might do some Google search, like I did infinite times, encountering the BeforeAllCallback
and AfterAllCallback
interfaces which are Extensions of Testing Lifecycle Callbacks. It seems a good solution as these interfaces enable you to run the @BeforeAll
or @AfterAll
.
The first try will be implementing an extension to simulate the @BeforeSuite
and @AfterSuite
from TestNG, as you want to run a pre and post-condition before all and any tests can be executed.
public class MyExtension implements BeforeAllCallback, AfterAllCallback { @Override public void afterAll(ExtensionContext context) { // pre general condition } @Override public void beforeAll(ExtensionContext context) { // post general conditions } }
The UML diagram shows a BaseTest
using a JUnit 5 Extension called MyExtension that implements the BeforeAllCallback
and AfterAllCallback
. The base test is used in the FeatureTest1 and FeatureTest2. Note that the MyExtension
has the beforeAll
and afterAllMethods
, as well as the BaseTest
has it.
It solves the problem as the MyExtension
would serve as the global before and after, as the ones in the BaseTest would run per test instance, meaning running when Feature1Test
and Feature2Test
run. Unfortunately, it’s not the case. If we would add only a System.out.println()
call in each method, the output would be the following:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.eliasnogueira.feature1.Feature1Test MyExtension.beforeAll BaseTest.beforeAll Feature1Test.test1 Feature1Test.test2 Feature1Test.test3 BaseTest.afterAll MyExtension.afterAll [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s -- in com.eliasnogueira.feature1.Feature1Test [INFO] Running com.eliasnogueira.feature2.Feature2Test MyExtension.beforeAll BaseTest.beforeAll Feature2Test.test1 Feature1Test.test2 BaseTest.afterAll MyExtension.afterAll [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.eliasnogueira.feature2.Feature2Test
You can see that the methods in the MyExtension
run for every test class, as well as the BaseTest. This means that JUnit runs it per test instance. Unfortunately, JUnit 5 doesn’t have a solution for a general precondition before or after any test for the whole test execution.
Want to see it in action?
– Clone this repo: git clone https://github.com/eliasnogueira/junit5-before-after-all
– Switch to the no-solution
branch
– Run mvn test
How to solve it
There is a light at the end of the tunnel and it’s not difficult. Like everyone, I Google it and found an interesting workaround on this Stackoverflow thread: https://stackoverflow.com/questions/43282798/in-junit-5-how-to-run-code-before-all-test.
Be aware that this is a workaround and might not work in future JUnit versions.
The solution is based on using the BeforeAllCallback
interface, with a thread lock in case parallel tests run to solve the general precondition, and the JUnit storage mechanism to mimic the postcondition using the ExtensionContext.Store.CloseableResource
interface. Don’t worry, I will break down the implementation.
The example
It is a simple one just to show you that the approach works. The example shows a general BaseTest and a BaseTest per feature, where an extension will be created to give the ability of execution a general pre and postcondition.
The extension implementation
The implementation can be done in four steps, and the final solution will look like this:
public class SetupExtension implements BeforeAllCallback, Store.CloseableResource { private static final Lock LOCK = new ReentrantLock(); private static volatile boolean started = false; @Override public void beforeAll(ExtensionContext context) { LOCK.lock(); try { if (!started) { started = true; System.out.println("[pre-condition] Should run only once"); context.getRoot().getStore(Namespace.GLOBAL).put("Placeholder for post condition", this); } } finally { // free the access LOCK.unlock(); } } @Override public void close() { System.out.println("[post-condition] Should run only once"); } }
Implementation of the necessary interfaces
Line 1 shows that the two interfaces: BeforeAllCallback
overriding the beforeAll()
method which will control the general precondition and ExtensionContext.Store.CloseableResource
overrides the close()
method which will mimic the general postcondition.
Controlling the single execution of the beforeAll
To guarantee that it will be executed only once one strategy must be applied: control that it has started, so the beforeAll()
won’t execute again.
Line 8 shows that we are locking the thread. This is necessary to ensure that any parallel execution will be possible. Line 11 checks if the code had any previous execution. When it’s the first time the boolean
started
is set as true
ensure it won’t go to the code block in the subsequent runs. The finally
section unlocks the thread.
Implementing the general precondition
Any necessary implementation for the general precondition should be placed inside the if
condition, simple like that. We can see this in the line 13.
Add a signal (storage) to mimic the general postcondition
The way to mimic the general postcondition here is through the Store. In JUnit 5, we can store objects for later retrieval in an extension and it can be done using the getStore(context).put(key, value)
where the context
is the root or current context and the key,value
are the key and value to add to its storage.
Line 14 creates a dummy store object for later usage in the automatic close()
method invocation.
Implement the general postcondition
The close()
method from the ExtensionContext.Store.CloseableResource
interface will be invoked when the extension lifecycle ends [reference]. This is the last opportunity to execute any code before the program exits. In this way, we can simulate the general postcondition.
Code example
The https://github.com/eliasnogueira/junit5-before-after-all project shows the implementation based on this article’s explanation matching the diagram in “The example” section.
While running the tests you will see the following output:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.eliasnogueira.feature1.Feature1Test [pre-condition] Should run only once BaseTest.beforeAll BaseFeature1Test.beforeAll Feature1Test.test1 Feature1Test.test2 Feature1Test.test3 BaseFeature1Test.afterAll BaseTest.afterAll [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.019 s -- in com.eliasnogueira.feature1.Feature1Test [INFO] Running com.eliasnogueira.feature2.Feature2Test BaseTest.beforeAll BaseFeature2Test.beforeAll Feature2Test.test1 Feature1Test.test2 BaseFeature2Test.afterAll BaseTest.afterAll [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.eliasnogueira.feature2.Feature2Test [post-condition] Should run only once
Note that the general precondition is the text output as [pre-condition] Should run only once
and the general postcondition is the output as [post-condition] Should run only once
. You can see them at the beginning and the end of all test executions, respectively.
Using this simple approach you can have the general pre and postcondition in your code.
Happy tests!