Skip to content

Introducing JUnit5

Estimated time to read: 9 minutes

from JUnit 5 Fundamentals

Introduction

JUnit is one of the most important Java frameworks, as it is the standard tool for unit testing within Java. It as played a big role in the development of agile methodologies. However, it is just another tool in the Java development toolset.

"Never in the field of software development was so much owed by so many to so few lines of code." - Martin Fowler

Types of Tests

There are many ways to test an application. To name a few;

  • Acceptance
  • Integration
  • Load
  • System
  • Regression
  • Stress
  • Recovery
  • Security
  • Usability
  • Unit

According the our Knowledge of the System, it is possible to use either black box testing or white box testing.

Unit Tests

  • Testing is all about getting feedback, feedback can tell you if your code is working as expected or not.
  • Having tests that give feedback, but are also easy to understand, read and modify is important.
  • Unit tests are written by programmers
  • Unit tests test a section of code by invoking it and checking the correctness of some assumptions.
  • Unit tests can be programmed to test methods or even entire classes.

Good Unit Test Practices

A good unit test should focus on productivity. It should be automated, easy to run, repeatable and fast.

Automated, fast, repeatable tests can be run as often as required, without harming overall productivity. By doing so, feedback is given that allows to detect and correct an issue as early as possible.

Dependant Services

If there is a dependency on a database or an external service, the test may not be as fast, as these dependencies are slower the just running the class locally. With many tests in place, execution time adds up.

Integration Tests

With integration tests, external components are tested to ensure that they work with the local code.

If the test depends on an external component to be available, it may not be as automated or repeatable as expected. This is the main difference between a unit and integration test.

Instead, a dummy or mock object that simulates the component to be accessed can be used instead, which isolates the code and component and puts it under the local users control.

Why unit tests are important

  • Reduce debugging time - A good suite of tests and good practices can reduce bugs and debug time.
  • Serve as documentation - Unit tests are written in code. This code can expose how the library is expected to work.
  • Improve the design - Good tests can assist in the design of a larger code base. Tests ensure that your application functions as expected, so code can typically be written around these assumptions. Writing tests first then writing code around them to satisfy the requirements of the application is known as Test-Driven Development or TDD.

Looking at the benefits of TDD above, it may seem that TDD is hard to approach or time consuming. However, the benefits outweigh the time taken to create the test suite, as it will reduce the time required to debug and issues with the application further down the line.

A terribly designed system with a well designed test suite implementation is far superior to a well designed system with terrible test suite implementation.

What is JUnit?

"JUnit is a simple, open-source framework to write and run repeatable tests." - JUnit Website

JUnit works by following the following principles:

  • A class is written to test assumptions of another class or method within your application.
  • The component being tested is known as the Subject Under Test or SUT.
  • When the test runs, JUnit checks to see if the assumptions either succeed or fail against the SUT
  • JUnit then informs the user of the result.

Screenshot 2022-08-11 at 10.42.19.png

JUnit Architecture

JUnit 4 and prior

JUnit 5, when compared to JUnit4, is more of an evolutionary release than a revolutionary release when compared to previous JUnit releases.

With versions of JUnit prior to JUnit 5; JUnit was included as a single JAR file with all components included, regardless of the requirements of the end user. This increased the size of projects, included code that was not relevant to a project and also provided no API flexible API support.

As JUnit's popularity increased, so did its usage within IDEs. However, the API in place at the time was not designed around using JUnit within an IDE. The resulted in IDEs using either internal JUnit classes or reflection for their purposes.

JUnit 5

"The success of JUnit as a platform prevents the development of JUnit as a tool." - Johannes Link

For JUnit 5, developers of the project looked to decouple test execution from other areas such as test definition, whilst also providing support for IDEs and other tools in an extensible way.

This lead to the development of JUnit 5 which included:

  • An API to write tests
  • A mechanism to discover and run tests
  • An API to run tests from Tools and IDE's

...whilst also implementing the following design goals:

  • Decouple Tests - Decoupling test execution from definition, users can mix tests from different libraries whilst still using the APIs that JUnit 5 provides for easier integration within IDE and other tools.
  • Preference for extension points - Instead of using runners, rules and sub-classing. Extensions can instead be used to keep JUnit core as simple as possible and enable backward compatibility.
  • Support for Java 8 features - Such as lambdas, streams and default methods in interfaces.

JUnit 5 Modules

Unlike previous versions of JUnit, JUnit 5 is composed of several different modules from several different projects.

Unit Platform

  • Provides an API to launch tests from either a console, IDE or other tools.
  • Provides an API for developing a testing framework that runs on the platform.

Libraries

  • junit-platform-commons - Includes common APIs and platform utilities.
  • junit-platform-console - For discovering and executing test from the console.
  • junit-platform-engine - An API for test engines.
  • junit-platform-launcher - An API for configuring and launching tests. Typically used by IDE's and build tools.
  • junit-platform-jfr - Provides support for generating flight recorder events.
  • junit-platform-suite - For configuring test suites on the JUnit platform.

Unit Jupiter

  • For writing tests and extensions.
  • Provides an engine for running Jupiter based tests on the platform.

Libraries

  • junit-jupiter-api - The API for writing test and extensions.
  • junit-jupiter-engine - The JUnit Jupiter engine implementation.
  • junit-jupiter-params - Provides support for parametrised tests.
  • junit-jupiter-migrationsupport - Provides migration support for selected JUnit 4 rules to the new extension model of JUnit 5 .

JUnit Vintage

  • Provides an engine for running JUnit 3 and 4 based tests under the updated platform.

Libraries

  • junit-vintage-engine - *The engine implementation for running JUnit 3 and 4 tests.

JUnit 5 Architecture

IDE Support

All major IDE's include JUnit support. These include, but are not limited to:

  • IntelliJ - Since version 2016.2
  • Eclipse - Since Oxygen 4.7.1a
  • NeatBeans - Since 10.0
  • VSCode - Via Extension support

JUnit Dependency

JUnit 5 can be included within a project by including it as a dependency.

This is done my importing the JUnit Jupiter API into the code of the application and using the following dependency structure.

  • groupId: org.junit.jupiter
  • ArtifactID: junit-jupiter-api
  • Version: 5.x.x

Note

Ensure that JUnit5 and its associated modules are included within the CLASSPATH for Java.

JUnit 5 import example

JUnit import
import org.junit.jupiter.api.Test;

public class HellWorldTest {
 @Test // Note that the test method is annotated with @Test
 void firstTest() {
  System.out.println("First Test");
 }
}

Note

In JUnit 5, classes and methods that are due to be tested are no longer required to be public, as needed in prior versions.

Note

Test methods are required to be tagged with the @Test annotation

Setting up JUnit with Gradle

Configuring JUnit 5 within a Gradle project follows the same standards as adding other dependencies to projects.

Starting with version 4.6, Gradle provides native support for JUnit 5. However, it needs to be enabled within build.gradle.

build.gradle
plugins {
 id = 'java'
}

group 'io.entityfour'
version '1.0-SNAPSHOT'

repositories {
 mavenCentral()
}

dependencies {
 testImplementation(platform('org.junit.junit-bom:5.x.x')) // Including the JUnit BOM will declare the libraries for the listed version, meaning that versions do not need to be included when adding further dependencies below.
 testImplementation('org.junit.jupiter:junit-jupiter') // Declaring juni-jupiter aggregates dependencies that are required to use JUnit 5 along with some optional dependencies. 
}
test {
 useJUnitPlatform()
}

Tests can then be run directly within an IDE or manually by running gradlew test.

JUnit 5 will output the results of testing to ./build/reports/tests/test/index.html

Setting up JUnit with Maven

Configuring JUnit 5 within a Maven project follows the same standards as adding other dependencies to projects.

pom.xml
...
 <dependencyManagement>
  <dependencies>
   <dependency> // Configure the JUnit BOM, this will declare the libraries needed for the listed version, meaning that versions do not need to be included when adding further dependencies below.
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>5.x.x</version>
    <type>pom</type>
    <scope>import</scope>
   </dependency>
  </dependencies>
 </dependencyManagement>

 <dependencies>
  <dependency> // Declaring juni-jupiter aggregates dependencies that are required to use JUnit 5 along with some optional dependencies. 
   <groupId>org.junit.jupiter</groupId>
   <artifiactId>junit-jupiter</artifiactId> 
   <scope>test</scope>
  </dependency>
 </dependencies>

// If running tests within an IDE, skip the below section.
// Else, If running tests directly with Maven requires an additional plugin.

 <build>
  <plugins>
   <plugin>
    <artifiactId>maven-surefire-plugin</artifiactId>
    <version>2.22.2</version>
   </plugin>    
  </plugins>
 </build>

Tests can then be run directly within an IDE or manually by running mvnw test.

JUnit platform

One of the main goals of JUnit 5 is to facilitate the integration of JUnit and other tools, such as IDE;s.

Launcher API

JUnit 5 introduced the concept of a Launcher API which can integrate with IDE's that support it. The Launcher API:

  • Assists with discovery, filtering and execution of tests
  • The API resides in the junit-platform-launcher module
  • Allows providing a custom test engine
  • Enabling third-party test libraries
  • Allows declaring the engine within IDE's to overwrite any inbuilt versions

Alternatives to the Launcher API

For IDE's without JUnit Support, there are two options available for using JUnit 5.

ConsoleLauncher

See ADDLINKHERE for more information.

JUnit platform Runner

  • If the IDE / tool supports JUnit 4, this can be used to run Jupiter tests as if they were JUnit 4 tests.
  • Allows definition of test suites, enabling multiple test classes to be run in a single pass.
  • Deprecated in JUnit Platform 1.8 and will be removed in the next major version of the JUnit platform.

If JUnit 5 tests are required to be run within a JUnit 4 environment, the JUnit Platform Runner can be found in its own JAR file. This has the be added to the project using the following dependency structure.

  • groupId: org.junit.platform
  • ArtifactID: junit-platform-runner
  • Version: 1.x.x

By adding the platform runner as a dependency to the project, running a JUnit4 test within JUnit 5 is as simple as adding an annotation.

RunWithJUnit4
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class) // Deprecated in JUnit 5.8
public class HelloWorldTest {
 @Test
 void firstTest() {
  System.out.println("First Test");
 }
}

Test Suites

JUnit 5 can import a series of unit tests by including the @Suite annotation in addition to the test engine and its dependencies, using the following dependency structure.

  • groupId: org.junit.platform
  • ArtifactID: junit-platform-suite
  • Version: 1.8.x

The dependency above aggregates the required dependencies for junit-platform-suite , which contains junit-platform-suite-api for configuring test based annotations, and junit-platform-suite-engine, which contains the test engine API for declarative test suites.

JUnit5 Test Suites
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDispalyName;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.IncludeTags;

@Suite
@SuiteDisplayName("Gift Services Tests")
@SelectPackages("io.entityfour.services.gift")
@IncludeTags("gift")
public class GiftServicesTest {}

In JUnit 5, JUnit 4 test suites can be included by importing the relevant libraries into the application and then annotating the class or method using the @RunWith() annotation, along with the same relevant annotations from the JUnit5 example

JUnit4 Test Suites
import org.junit.runner.RunWith;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SuiteDispalyName;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.IncludeTags;

@RunWith(JUnitPlatform.class) // Deprecated in JUnit 5.8
@SuiteDisplayName("Gift Services Tests")
@SelectPackages("io.entityfour.services.gift")
@IncludeTags("gift")
public class GiftServicesTest {}