Lesson 1: Lifecycle Methods

1. Overview

In this lesson, we'll learn about the lifecycle methods in JUnit 5 and how to use them in our tests.

 

The relevant module you need to import when you're starting with this lesson is: lifecycle-methods-start.

If you want to have a look at the fully implemented lesson as a reference, feel free to import: lifecycle-methods-end.

2. Lifecycle Methods

We often have to perform some setup or teardown steps that are common to all the tests in a class. In order to avoid repeating this logic on each test, we can use the Lifecycle Methods for a DRYer approach.

A lifecycle method is any method that is annotated with one of the @BeforeEach, @AfterEach, @BeforeAll, or @AfterAll annotations. Lifecycle methods can be declared locally or inherited from superclasses or interfaces, and are subject to a few restrictions:

  • must not be abstract
  • must not return a value (return type must be void)
  • must not be private

Just to be able to clearly appreciate how the lifecycle methods are executed, we won't be actually testing anything at this stage, but instead, we'll consider a naive scenario in which our system needs to open and read the contents of the file.txt file, like the one present in the src/test/resources directory:

Now, let's create an empty com.baeldung.lju.LifecycleMethodsAndResourcesHandlingUnitTest test class and proceed to discuss each annotation.

2.1. @BeforeEach

The @BeforeEach annotation indicates to JUnit that it should execute the annotated method before running each test in the current class. We typically use this lifecycle method when we have some common setup logic to run before each test.

Let's imagine that we need to access the contents of the file.txt file before each test:

This mechanism greatly simplifies our test cases by avoiding the need to repeat this rather complex logic on each test.

Let’s also add a simple test method that prints to the console two lines from the file.txt file:

It's important to manage resources correctly when writing tests, so we're also closing the fileReader in the last step of the test.
We'll see the following output printed to the console when running the test:

We can notice that the setupUsingResource method, which is annotated with @BeforeEach, is executed before the test.
Now, let's take things further and add another test in the same class, which also prints two lines from the file:

Running the LifecycleMethodsAndResourcesHandlingUnitTest test class produces the output below:

We can observe that JUnit executed the @BeforeEach method twice, once before each test, and that in both cases, the first two lines of the file are logged.

2.2. @AfterEach

The @AfterEach annotation indicates to JUnit that it should execute the annotated method after the execution of each test in the current class. We typically use this lifecycle method when we have some common cleanup logic to run after each test.

In the previous example, we already have some common cleanup logic in each test which closes the fileReader. Let's move this common logic to a separate method:

We’ll see the following output printed to the console when running the updated test class:

We can notice that JUnit executed the @AfterEach annotated method twice, once after each test.

2.3. @BeforeAll

There are times when the setup required for tests is expensive, and we'd like to run it only once for all tests in a class. Typical scenarios include fetching external data, connecting or setting up a remote or embedded database, or starting a local container or an Apache Kafka server for the integration tests. This is the perfect use case for the @BeforeAll annotation, which indicates to JUnit that it should execute the annotated method only once before running any test in the current class.

Let's refactor our tests to open the file only once and explore the effects of such an operation:

Note that the setupResource method and fileReader class variable are now static. This means that the fileReader class variable is now shared across all the tests, therefore we also need to keep the stream open while running all the tests:

Running the updated LifecycleMethodsAndResourcesHandlingUnitTest class produces the output below:

Inspecting the output, we can see that JUnit executed the @BeforeAll annotated method only once for both tests, in contrast to the @BeforeEach and @AfterEach annotated methods. We can also notice that the second test continued reading the file contents from where the first test left it. Therefore, the console output now contains all the contents of the file.

2.4. @AfterAll

The @AfterAll annotation indicates to JUnit that it should execute the annotated method only once after running all the tests in the current class. Typical scenarios include cleanup tasks after the integration tests, like closing a database connection, shutting down a container, and freeing up temporary resources.

Let's include the cleanup task for the fileReader class variable:

Note that the method annotated with @AfterAll is static. Running the updated LifecycleMethodsAndResourcesHandlingUnitTest class produces the output below:

Looking at the last line in the output, we can see that JUnit executed the @AfterAll annotated method only once for all tests, in contrast to the @BeforeEach and @AfterEach annotated methods.

3. Testing Our Functionality

We’ve seen the different lifecycle methods and how they are executed individually. Now, let’s actually analyze how we can improve our tests by making use of these lifecycle methods.

First, let's consider the InMemoryCampaignRepositoryUnitTest class using the @BeforeEach and @AfterEach annotations:

Of course, we're adding some logging here only for academic purposes. We can see that the campaignRepository class variable is correctly initialized by running the setupDataSource method before executing each test, and the cleanup method is also executed after executing each test.

We can also override the repository variable if it makes sense for specific scenarios:

Depending on our requirements,we could also opt for using the @BeforeAll and @AfterAll annotations. Let's create a new com.baeldung.lju.persistence.repository.impl,InMemoryCampaignRepositoryWithStaticResourceUnitTest class:

However, it's important to point out that we're using a shared variable here, so any changes performed on a test could impact the context for the others.

4. Conclusion

The examples provided in this lesson demonstrate that a test class can have methods annotated with *Each, methods annotated with *All, or both types of methods.

When isolation is a critical requirement for our tests, we should use the *Each annotations. However, this comes at a cost, and it can significantly increase the time required to run the tests.

We should consider the *All annotations when having a shared state between tests is not an issue or if the required test setup is expensive. When using objects that are shared between tests, it is a good idea to ensure that no side effects are propagated from the tests. We can achieve this by using immutable objects.

Complete and Continue