Junit 5 - Test Instance Lifecycle

dimitrilc 2 Tallied Votes 200 Views Share

Introduction

Junit is a popular framework to create tests for Java applications. Although individual unit tests are mostly straightforward, integration and functional tests are a little bit more involved because they usually require multiple components working together. For that reason, understanding the life cycle of Junit tests can be greatly beneficial.

In this tutorial, we will learn about the lifecycle of Junit 5 test instances.

Goals

At the end of the tutorial, you would have learned:

  1. Different stages of a Junit instance lifecycle.

Prerequisite Knowledge

  1. Basic Java.
  2. Basic Junit.

Tools Required

  1. A Java IDE such as IntelliJ Community Edition.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Gradle Java project. I am using Java 17 and Gradle 7.2, but any Java version 8+ should work.
  2. Add the dependency for unit-jupiter-engine. The latest version is 5.8.1 as of this writing.

Below is the content of my build.gradle file.

plugins {
   id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
   mavenCentral()
}

dependencies {
   testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
   useJUnitPlatform()
}
  1. Under src/test/java, create a new package called com.example.
  2. Under com.example, create a new class called LifecycleTest.
  3. We do not need the main() method for this tutorial.

Lifecycle Overview

There are 5 stages of a Junit 5 test instance lifecycle. The list below orders them from first to last. The @ symbol means that there is an annotation matching that lifecycle stage as well.

  1. @BeforeAll: executed before all tests.
  2. @BeforeEach: executed before each test.
  3. @Test: the test itself.
  4. @AfterEach: executed after each test.
  5. @AfterAll: executed after all tests.

To see how they work together, we need to add some tests into our code. In LifecycleTest.java, add the code below.

package com.example;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

//@TestInstance(PER_CLASS)
class LifecycleTest {

   @BeforeAll
   void beforeAll(){
       System.out.println("Before All");
   }

   @BeforeEach
   void beforeEach(){
       System.out.println("Before Each");
   }

   @Test
   void test(){
       System.out.println("Test");
   }

   @AfterEach
   void afterEach(){
       System.out.println("After Each");
   }

   @AfterAll
   void afterAll(){
       System.out.println("After All");
   }
}

But there is a problem with the code above. If we run the test, it will actually throw a JunitException.

@BeforeAll method 'void com.example.LifecycleTest.beforeAll()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).

The reason for the JunitException is that the methods annotated with @BeforeAll and @AfterAll must either be static or the enclosing class must be annotated with @TestInstance(Lifecycle.PER_CLASS). By default, Junit creates a new instance of LifecycleTest for every test with @TestInstance(Lifecycle.PER_METHOD). So if we want the code to run correctly, we would need to add @TestInstance(Lifecycle.PER_CLASS) on top of the LifecycleTest class declaration.

Go ahead and uncomment the line

//@TestInstance(Lifecycle.PER_CLASS)

in the code. When we run the test again, we should now see the test passing and the output prints out the lines in expected order.

Before All
Before Each
Test
After Each
After All

Repeated Tests

While the lifecycle is simple to understand when executing only a single test, repeated tests behave a little bit differently. When running repeated tests, @BeforeAll and @AfterAll are only executed once, while @BeforeEach and @AfterEach are always executed for each test. To demonstrate, let us comment out the @Test method in our code.

//    @Test
//    void test(){
//        System.out.println("Test");
//    }

And add a repeated test method. This test method will run twice because of the int value we passed to the @RepeatedTest annotation.

@RepeatedTest(2)
void repeatTest(){
   System.out.println("Repeat Test");
}

When we run the test, we will see @BeforeAll and @AfterAll were only executed once, while @BeforeEach and@AfterEach are both executed twice(I have added some line separators to make the output easier to read).

Before All

Before Each
Repeat Test
After Each

Before Each
Repeat Test
After Each

After All

Solution Code

package com.example;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;

@TestInstance(PER_CLASS)
class LifecycleTest {

   @BeforeAll
   void beforeAll(){
       System.out.println("Before All");
   }

   @BeforeEach
   void beforeEach(){
       System.out.println("Before Each");
   }

//    @Test
//    void test(){
//        System.out.println("Test");
//    }

   @RepeatedTest(2)
   void repeatTest(){
       System.out.println("Repeat Test");
   }

   @AfterEach
   void afterEach(){
       System.out.println("After Each");
   }

   @AfterAll
   void afterAll(){
       System.out.println("After All");
   }
}

Summary

We have learned about the different stages of a Junit 5 test instance lifecycle. The full project code can be found here https://github.com/dmitrilc/DaniwebJunitLifecycle/tree/master