This tutorial explains unit testing with JUnit with the JUnit 5 framework (JUnit Jupiter). It explains the creation of JUnit 5 tests with the Maven and Gradle build system. It demonstrates the usage of the Eclipse IDE for developing software tests with JUnit 5 but this tutorial is also valid for tools like Visual Code or IntelliJ.
1. Overview
JUnit is a widely-used unit testing framework in the Java ecosystem. With the release of JUnit 5, many new features were introduced, leveraging the capabilities of Java 8 and beyond.
This guide provides an introduction to unit testing with the JUnit framework, focusing specifically on the features and usage of JUnit 5. For further information about testing with Java see:
1.1. Configuration for using JUnit 5
To use JUnit 5 you have to make the libraries available for your test code. Jump to the section which is relevant to you, for example read the Maven part, if you are using Maven as build system.
Configure Maven to use JUnit 5
== Configure Maven dependencies for JUnit 5
=== Steps required to configure Maven to use JUnit5
To use JUnit5 in an Maven project, you need to:
-
Configure to use Java 11 or higher
-
Configure the maven-surefire-plugin and maven-failsafe-plugin to a higher version as the default (example uses 3.5.0) so that it can run JUnit5
-
Add dependencies to the JUnit5 API and engine for your test code
=== Configure Maven
Therefore you need to adjust your pom file, similar to the following:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!--1 -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
<!--2 -->
<dependencies>
<!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest-library -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Once you have done this, you can start using JUnit5 in your Maven project for writing unit tests.
=== Update Maven settings (in case you are using the Eclipse IDE)
Right-click your pom file, select
and select your project. This triggers an update of your project settings and dependencies.Configure Gradle to use JUnit 5
== Update build.gradle file to use JUnit5
To use JUnit 5 with the Gradle build system, ensure you use at least Gradle 6.0 to avoid already fixed issues.
Modify your build.gradle file to contain at least the following entries. Your build file may contain more dependencies.
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}
test {
useJUnitPlatform()
Configure JUnit 5 with Eclipse
If you are not using a build system and the JUnit library is not part of the classpath of your project during the creation of a new test, Eclipse prompts you to add it.
1.2. How to define a test in JUnit?
A JUnit test is a method contained in a class which is only used for testing.
This is called a Test class.
To mark a method as a test method, annotate it with the @Test
annotation.
This method executes the code under test.
The following code defines a minimal test class with one minimal test method.
package com.vogella.junit.first;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class AClassWithOneJUnitTest {
@Test
void demoTestMethod() {
assertTrue(true);
}
}
You can use assert methods, provided by JUnit or another assert framework, to check an expected result versus the actual result. Such statement are called asserts or assert statements.
Assert statements typically allow to define messages which are shown if the test fails. You should provide here meaningful messages to make it easier for the user to identify and fix the problem. This is especially true if someone looks at the problem, who did not write the code under test or the test code.
1.3. Example for developing a JUnit 5 test for another class
The following example defines a Java class and defines software tests for it.
Assume you have the following class which you want to test.
package com.vogella.junit5;
public class Calculator {
public int multiply(int a, int b) {
return a * b;
}
}
A test class for the above class could look like the following.
package com.vogella.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
class CalculatorTest {
Calculator calculator;
@BeforeEach (1)
void setUp() {
calculator = new Calculator();
}
@Test (2)
@DisplayName("Simple multiplication should work") (3)
void testMultiply() {
assertEquals(20, calculator.multiply(4, 5), (4)
"Regular multiplication should work"); (5)
}
@RepeatedTest(5) (6)
@DisplayName("Ensure correct handling of zero")
void testMultiplyWithZero() {
assertEquals(0, calculator.multiply(0, 5), "Multiple with zero should be zero");
assertEquals(0, calculator.multiply(5, 0), "Multiple with zero should be zero");
}
}
1 | The method annotated with @BeforeEach runs before each test |
2 | A method annotated with @Test defines a test method |
3 | @DisplayName can be used to define the name of the test which is displayed to the user |
4 | This is an assert statement which validates that expected and actual value is the same, if not the message at the end of the method is shown |
5 | @RepeatedTest defines that this test method will be executed multiple times, in this example 5 times |
1.4. JUnit test class naming conventions
Build tools like Maven use a pattern to decide if a class is a test classes or not. The following is the list of classes Maven considers automatically during its build:
**/Test*.java (1)
**/*Test.java (2)
**/*Tests.java (3)
**/*TestCase.java (4)
1 | includes all of its subdirectories and all Java filenames that start with Test . |
2 | includes all of its subdirectories and all Java filenames that end with Test . |
3 | includes all of its subdirectories and all Java filenames that end with Tests . |
4 | includes all of its subdirectories and all Java filenames that end with TestCase . |
Therefore, it is common practice to use the Test or Tests suffix at the end of test classes names.
1.5. Where should the test be located?
Typical, unit tests are created in a separate source folder to keep the test code separate from the real code. The standard convention from the Maven and Gradle build tools is to use:
-
src/main/java - for Java classes
-
src/test/java - for test classes
1.6. Static imports and unit testing
JUnit 5 allows to use static imports for its assertStatements to make the test code short and easy to read.
Static imports are a Java feature that allows fields and methods defined in a class as public static
to be used without specifying the class in which the field is defined.
JUnit assert statements are typically defined as public static
to allow the developer to write short test statements.
The following snippet demonstrates an assert statement with and without static imports.
// without static imports you have to write the following statement
import org.junit.jupiter.api.Assertions;
// more code
Assert.assertEquals("10 x 5 must be 50", 50, tester.multiply(10, 5));
// alternatively define assertEquals as static import
import static org.junit.jupiter.api.Assertions.assertEquals;
// more code
// use assertEquals directly because of the static import
assertEquals(calculator.multiply(4,5), 20, "Regular multiplication should work");
2. Assertions and assumptions
JUnit 5 comes with multiple assert statements, which allows you to test your code under test.
Simple assert statements like the following allow to check for true, false or equality.
All of them are static methods from the org.junit.jupiter.api.Assertions.*
package.
Assert statement | Example |
---|---|
assertEquals |
assertEquals(4, calculator.multiply(2, 2),"optional failure message"); |
assertTrue |
assertTrue('a' < 'b', () → "optional failure message"); |
assertFalse |
assertFalse('a' > 'b', () → "optional failure message"); |
assertNotNull |
assertNotNull(yourObject, "optional failure message"); |
assertNull |
assertNull(yourObject, "optional failure message"); |
Messages can be created via lambda expressions, to avoid the overhead in case the construction of the message is expensive.
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
2.1. Testing for exceptions
Testing that certain exceptions are thrown are be done with the org.junit.jupiter.api.Assertions.assertThrows()
assert statement.
You define the expected Exception class and provide code that should throw the exception.
package com.vogella.junit.first;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class UserTest {
private User user;
@BeforeEach
void setup() {
user = new User();
}
@Test
void exceptionTesting() {
// set up user
Throwable exception = assertThrows(IllegalArgumentException.class, () -> user.setAge("A"));
assertEquals("Age must be an Integer.", exception.getMessage());
}
// This is date used for the test
private class User {
// not really a setter, just here to demonstrate how the test works
public void setAge(String ageString) {
try {
Integer.parseInt(ageString);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Age must be an Integer.");
}
}
}
}
This lets you define which part of the test should throw the exception. The test will still fail if an exception is thrown outside of this scope.
2.2. Testing multiple assertions (grouped assertions) with assertAll
If an assert fails in a test, JUnit will stop executing the test and additional asserts are not checked.
In case you want to ensure that all asserts are checked you can assertAll
.
In this grouped assertion all assertions are executed, even after a failure. The error messages get also grouped together.
@Test
void groupedAssertions() {
Address address = new Address();
assertAll("address name",
() -> assertEquals("John", address.getFirstName()),
() -> assertEquals("User", address.getLastName())
);
}
If these tests fail, the result looks like the following:
=> org.opentest4j.MultipleFailuresError: address name (2 failures)
expected: <John> but was: <null>
expected: <User> but was: <null>
2.3. Defining timeouts in your tests
If you want to ensure that a test fails, if it isn’t done in a certain amount of time you can use the assertTimeout()
method.
This assert fails the method if the timeout is exceeded.
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static java.time.Duration.ofSeconds;
import static java.time.Duration.ofMinutes;
@Test
void timeoutNotExceeded() {
assertTimeout(ofMinutes(1), () -> service.doBackup());
}
// if you have to check a return value
@Test
void timeoutNotExceededWithResult() {
String actualResult = assertTimeout(ofSeconds(1), () -> {
return restService.request(request);
});
assertEquals(200, request.getStatus());
}
=> org.opentest4j.AssertionFailedError: execution exceeded timeout of 1000 ms by 212 ms
If you want your tests to cancel after the timeout period is passed you can use the assertTimeoutPreemptively()
method.
@Test
void timeoutNotExceededWithResult() {
String actualResult = assertTimeoutPreemptively(ofSeconds(1), () -> {
return restService.request(request);
});
assertEquals(200, request.getStatus());
}
=> org.opentest4j.AssertionFailedError: execution timed out after 1000 ms
Such a test might be flacky, in case the test server is busy, the test execution might take longer and therefore such a test might fails from time to time. |
2.4. How to disable tests
The @Disabled
or @Disabled("Why disabled")
annotation marks a test to be disabled.
This is useful when the underlying code has been changed and the test case has not yet been adapted of if the test demonstrates an incorrect behavior in the code which has not yet been fixed.
It is best practice to provide the optional description, why the test is disabled.
Alternatively you can use Assumptions.assumeFalse
or Assumptions.assumeTrue
to define a condition for test execution.
Assumptions.assumeFalse
marks the test as invalid, if its condition evaluates to true.
Assumptions.assumeTrue
evaluates the test as invalid if its condition evaluates to false.
For example, the following disables a test on Linux:
Assumptions.assumeFalse(System.getProperty("os.name").contains("Linux"));
This gives TestAbortedException
which the test runners evaluate as skipped tests.
For example the following testMultiplyWithZero
is skipped if executed on Linux.
package com.vogella.junit5;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
public class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() throws Exception {
calculator = new Calculator();
}
@RepeatedTest(5)
@DisplayName("Ensure correct handling of zero")
void testMultiplyWithZero() {
Assumptions.assumeFalse(System.getProperty("os.name").contains("Linux"));
assertEquals(calculator.multiply(0,5), 0, "Multiple with zero should be zero");
assertEquals(calculator.multiply(5,0), 0, "Multiple with zero should be zero");
}
}
You can also write an extension for @ExtendWith which defines conditions under which a test should run.
3. Dynamic and parameterized tests
JUnit 5 supports the creation of dynamic tests via code. You can also run tests with a set of different input values with parameterized tests.
Both approaches are described here.
3.1. Using Dynamic Tests
Dynamic test methods are annotated with @TestFactory
and allow you to create multiple tests of type DynamicTest
with your code.
They can return:
-
an Iterable
-
a Collection
-
a Stream
JUnit 5 creates and runs all dynamic tests during test execution.
Methods annotated with @BeforeEach
and @AfterEach
are not called for dynamic tests.
This means, that you can’t use thesm to reset the test object, if you change it’s state in the lambda expression for a dynamic test.
In the following example we define a method to return a Stream of DynamicTest
instances.
package com.vogella.unittest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
class DynamicTestCreationTest {
@TestFactory
Stream<DynamicTest> testDifferentMultiplyOperations() {
MyClass tester = new MyClass();
int[][] data = new int[][] { { 1, 2, 2 }, { 5, 3, 15 }, { 121, 4, 484 } };
return Arrays.stream(data).map(entry -> {
int m1 = entry[0];
int m2 = entry[1];
int expected = entry[2];
return dynamicTest(m1 + " * " + m2 + " = " + expected, () -> {
assertEquals(expected, tester.multiply(m1, m2));
});
});
}
// class to be tested
class MyClass {
public int multiply(int i, int j) {
return i * j;
}
}
}
3.2. Using Parameterized Tests
Junit5 also supports parameterized tests.
To use them you have to add the junit-jupiter-params
package as a test dependencies.
Adding junit-jupiter-params
dependency for a Maven build
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
Adding junit-jupiter-params
dependency for a Gradle build
If you are using Gradle:
dependencies {
// .. your other dependencies
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.7.2'
}
For this example we use the @MethodSource
annotation.
We give it the name of the function(s) we want it to call to get it’s test data.
The function has to be static and must return either a Collection, an Iterator, a Stream or an Array.
On execution the test method gets called once for every entry in the data source.
In contrast to Dynamic Tests @BeforeEach
and @AfterEach
methods will be called for parameterized tests.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class UsingParameterizedTest {
public static int[][] data() {
return new int[][] { { 1 , 2, 2 }, { 5, 3, 15 }, { 121, 4, 484 } };
}
@ParameterizedTest
@MethodSource(value = "data")
void testWithStringParameter(int[] data) {
MyClass tester = new MyClass();
int m1 = data[0];
int m2 = data[1];
int expected = data[2];
assertEquals(expected, tester.multiply(m1, m2));
}
// class to be tested
class MyClass {
public int multiply(int i, int j) {
return i * j;
}
}
}
3.2.1. Data sources
The following table gives an overview of all possible test data sources for parameterized tests.
Annotation | Description |
---|---|
|
Lets you define an array of test values.
Permissible types are |
|
Lets you pass Enum constants as test class.
With the optional attribute |
|
The result of the named method is passed as argument to the test. |
|
Expects strings to be parsed as Csv. The delimiter is |
|
Specifies a class that provides the test data.
The referenced class has to implement the |
3.2.2. Argument conversion
JUnit tries to automatically convert the source strings to match the expected arguments of the test method.
If you need explicit conversion you can specify a converter with the @ConvertWith
annotation.
To define your own converter you have to implement the ArgumentConverter
interface.
In the following example we use the abstract SimpleArgumentConverter
base class.
@ParameterizedTest
@ValueSource(ints = {1, 12, 42})
void testWithExplicitArgumentConversion(@ConvertWith(ToOctalStringArgumentConverter.class) String argument) {
System.err.println(argument);
assertNotNull(argument);
}
static class ToOctalStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(Integer.class, source.getClass(), "Can only convert from Integers.");
assertEquals(String.class, targetType, "Can only convert to String");
return Integer.toOctalString((Integer) source);
}
}
4. Additional information about JUnit 5 usage
4.1. Nested tests
The @Nested
annotation can be used to annotate inner classes which also contain tests.
This allows to group tests and have additional @BeforeEach
method, and one @AfterEach
methods.
When you add nested test classes to our test class, the following rules must be followed:
-
All nested test classes must be non-static inner classes.
-
The nested test classes are annotated with
@Nested
annotation so that the runtime can recognize the nested test classes. -
a nested test class can contain
Test
methods, one @BeforeEach method, and one @AfterEach method.
Because Java doesn’t allow static members in inner classes, a nested class cannot have additional @BeforeAll and @AfterAll methods. There is no limit for the depth of the class hierarchy. |
4.2. Test execution order
JUnit runs test methods is a deterministic but unpreditable order (MethodSorters.DEFAULT). You can use the @TestMethodOrder on the class to control the execution order of the tests, via:
-
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) - Allows to use the @Order(int) annotation on methods to define order
-
@TestMethodOrder(MethodOrderer.DisplayName.class) - runs test method in alphanumeric order of display name
-
@TestMethodOrder(MethodOrderer.MethodName.class) - runs test method in alphanumeric order of method name
-
Custom implementation - Implement your own MethodOrderer via the orderMethods method, which allows you to call context.getMethodDescriptors().sort(..)
The following demonstrates this with OrderAnnotation.class
.
package com.vogella.unittest.sortmethods;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
class OrderAnnotationDemoTest {
@Test
@Order(1)
void firstOne() {
// test something here
}
@Test
@Order(2)
void secondOne() {
// test something here
}
}
4.3. Using the @TempDir annotation to create temporary files and paths
The @TempDir
annotations allows to annotate non-private fields or method parameters in a test method of type Path
or File.
JUnit 5 has registered a `ParameterResolutionException
for this annotation and will create temporary files and paths for the tests.
It will also remove the temporary files are each test.
@Test
@DisplayName("Ensure that two temporary directories with same files names and content have same hash")
void hashTwoDynamicDirectoryWhichHaveSameContent(@TempDir Path tempDir, @TempDir Path tempDir2) throws IOException {
Path file1 = tempDir.resolve("myfile.txt");
List<String> input = Arrays.asList("input1", "input2", "input3");
Files.write(file1, input);
assertTrue(Files.exists(file1), "File should exist");
Path file2 = tempDir2.resolve("myfile.txt");
Files.write(file2, input);
assertTrue(Files.exists(file2), "File should exist");
}
4.4. Test Suites
The 5.8 release of JUnit 5 is planned to have test suite support included.
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
public class SuiteDemo {
}
See https://github.com/junit-team/junit5/pull/2416 for the work.
At this time of writing you can use the milestone release of 5.8.0-M1 to check this. See the dependencies here https://search.maven.org/artifact/org.junit.platform/junit-platform-suite-api/1.8.0-M1/jar
5. Exercise: Writing a JUnit 5 test with Maven and Eclipse in 5 mins
In this exercise you learn you to write a JUnit5 test using Maven and the Eclipse IDE.
5.1. Project creation
Create a new Maven project with the following settings:
-
Group:
com.vogella
-
Artifact:
com.vogella.junit.first
-
Version:
0.0.1-SNAPSHOT
-
Packaging: jar
5.2. Configure Maven dependencies for JUnit 5
5.2.1. Steps required to configure Maven to use JUnit5
To use JUnit5 in an Maven project, you need to:
-
Configure to use Java 11 or higher
-
Configure the maven-surefire-plugin and maven-failsafe-plugin to a higher version as the default (example uses 3.5.0) so that it can run JUnit5
-
Add dependencies to the JUnit5 API and engine for your test code
5.2.2. Configure Maven
Therefore you need to adjust your pom file, similar to the following:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!--1 -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
<!--2 -->
<dependencies>
<!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest-library -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Once you have done this, you can start using JUnit5 in your Maven project for writing unit tests.
5.2.3. Update Maven settings (in case you are using the Eclipse IDE)
Right-click your pom file, select
and select your project. This triggers an update of your project settings and dependencies.5.3. Package creation
Ensure you have the package named com.vogella.junit.first in the src/main/java and src/main/test folder. If these are missing, create it.
5.4. Create a Java class
In the src folder, create the following class in the com.vogella.junit.first
package.
package com.vogella.junit.first;
public class MyClass {
// the following is just an example
public int multiply(int x, int y) {
if (x > 999) {
throw new IllegalArgumentException("X should be less than 1000");
}
return x / y;
}
}
5.5. Create a JUnit test
Position the cursor on the MyClass
in the Java editor and press Ctrl+1.
Select that you want to create a new JUnit test from the list.
Alternatively you can right-click on your new class in the :_Project Explorer_ or Package Explorer view and select | .
In the following wizard ensure that the New JUnit Jupiter test flag is selected. The source folder should select the test directory.
Press the Next button and select the methods that you want to test.
Create a test with the following code.
package com.vogella.junit.first;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class MyClassTest {
@Test
void testExceptionIsThrown() {
MyClass tester = new MyClass();
assertThrows(IllegalArgumentException.class, () -> tester.multiply(1000, 5));
}
@Test
void testMultiply() {
MyClass tester = new MyClass();
assertEquals(50, tester.multiply(10, 5), "10 x 5 must be 50");
}
}
5.6. Run your test in Eclipse
Right-click on your new test class and select
.The result of the tests are displayed in the JUnit view. In our example one test should be successful and one test should show an error. This error is indicated by a red bar.
You discovered a bug in the tested code!
5.7. Fix the bug and re-run your tests
The test is failing, because our multiplier class is currently not working correctly. It does a division instead of multiplication. Fix the bug and re-run the test to get a green bar.
Solution
package com.vogella.junit.first;
public class MyClass {
// the following is just an example
public int multiply(int x, int y) {
if (x > 999) {
throw new IllegalArgumentException("X should be less than 1000");
}
return x * y;
}
}
5.8. Review
After a few minutes you should have created a new project, a new class and a new unit test. Congratulations! If you feel like it, lets improve the tests a bit and write one grouped test.
5.9. Simplify your test code with @Before each
The initialization of MyClass
happens in every test, move the initialization to a @BeforeEach
method.
Solution
import org.junit.jupiter.api.BeforeEach;
// more import statements
class MyClassTest {
private MyClass tester;
@BeforeEach
void setup() {
tester = new MyClass();
}
// tests are before
}
5.10. Define a group check with assertAll
Define a new test method which checks both condition at the same time with assertAll
statement.
Change the condition to make both tests fail, run the test and ensure that both are executed.
Solution
@Test
public void testGrouped() {
assertAll( //
() -> assertThrows(IllegalArgumentException.class, () -> tester.multiply(1, 5)),
() -> assertEquals(501, tester.multiply(10, 5), "10 x 5 must be 50")
);
}
Afterwards adjust the test so that both are successfully executed.
6. Exercise: Writing a JUnit 5 test with Gradle and Eclipse in 5 mins
In this exercise you learn you to write a JUnit5 test using the Gradle build system and the Eclipse IDE.
6.1. Project creation
Create a new Gradle project with the following setting:
-
Name:
com.vogella.junit.first
See Create a Grade project with Eclipse to learn how to create a Gradle project with Eclipse.
The wizard should also have create the package com.vogella.junit.first in the src/main/java and src/main/test folder. Remove the generated classes from it.
6.2. Update build.gradle file to use JUnit5
To use JUnit 5 with the Gradle build system, ensure you use at least Gradle 6.0 to avoid already fixed issues.
Modify your build.gradle file to contain at least the following entries. Your build file may contain more dependencies.
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
}
test {
useJUnitPlatform()
6.3. Create a Java class
In the src folder, create the following class in the com.vogella.junit.first
package.
package com.vogella.junit.first;
public class MyClass {
// the following is just an example
public int multiply(int x, int y) {
if (x > 999) {
throw new IllegalArgumentException("X should be less than 1000");
}
return x / y;
}
}
6.4. Create a JUnit test
Position the cursor on the MyClass
in the Java editor and press Ctrl+1.
Select that you want to create a new JUnit test from the list.
Alternatively you can right-click on your new class in the :_Project Explorer_ or Package Explorer view and select | .
In the following wizard ensure that the New JUnit Jupiter test flag is selected. The source folder should select the test directory.
Press the Next button and select the methods that you want to test.
Create a test with the following code.
package com.vogella.junit.first;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class MyClassTest {
@Test
void testExceptionIsThrown() {
MyClass tester = new MyClass();
assertThrows(IllegalArgumentException.class, () -> tester.multiply(1000, 5));
}
@Test
void testMultiply() {
MyClass tester = new MyClass();
assertEquals(50, tester.multiply(10, 5), "10 x 5 must be 50");
}
}
6.5. Run your test in Eclipse
Right-click on your new test class and select
.The result of the tests are displayed in the JUnit view. In our example one test should be successful and one test should show an error. This error is indicated by a red bar.
You discovered a bug in the tested code!
6.6. Fix the bug and re-run your tests
The test is failing, because our multiplier class is currently not working correctly. It does a division instead of multiplication. Fix the bug and re-run the test to get a green bar.
Solution
package com.vogella.junit.first;
public class MyClass {
// the following is just an example
public int multiply(int x, int y) {
if (x > 999) {
throw new IllegalArgumentException("X should be less than 1000");
}
return x * y;
}
}
6.7. Review
After a few minutes you should have created a new project, a new class and a new unit test. Congratulations! If you feel like it, lets improve the tests a bit and write one grouped test.
6.8. Simplify your test code with @Before each
The initialization of MyClass
happens in every test, move the initialization to a @BeforeEach
method.
Solution
import org.junit.jupiter.api.BeforeEach;
// more import statements
class MyClassTest {
private MyClass tester;
@BeforeEach
void setup() {
tester = new MyClass();
}
// tests are before
}
6.9. Define a group check with assertAll
Define a new test method which checks both condition at the same time with assertAll
statement.
Change the condition to make both tests fail, run the test and ensure that both are executed.
Solution
@Test
public void testGrouped() {
assertAll( //
() -> assertThrows(IllegalArgumentException.class, () -> tester.multiply(1, 5)),
() -> assertEquals(501, tester.multiply(10, 5), "10 x 5 must be 50")
);
}
Afterwards adjust the test so that both are successfully executed.
7. Exercise: Writing JUnit5 unit tests
In this exercise, you develop some JUnit 5 tests for a given data model. You already learned how to create projects with Maven or Gradle and how to add Junit5 to them.
To review this information see:
The following description assumes that you are familiar with these steps and will not repeat them.
7.1. Create project and add JUnit5 dependencies
Create a new project called com.vogella.unittest
either with Maven or with Gradle and update their settings to use JUnit5.
7.2. Create the data model used for testing
Create the com.vogella.unittest.model
package and copy and paste the following classes on it.
package com.vogella.unittest.model;
import java.util.Date;
public class Movie {
private String title;
private Date releaseDate;
@SuppressWarnings("unused")
private String duration;
public Movie(String title, Date releaseDate, String duration) {
super();
this.title = title;
this.releaseDate = releaseDate;
this.duration = duration;
}
public String getTitle() {
return title;
}
public Date getReleaseDate() {
return releaseDate;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((releaseDate == null) ? 0 : releaseDate.hashCode());
result = prime * result + ((title == null) ? 0 : title.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Movie other = (Movie) obj;
if (releaseDate == null) {
if (other.releaseDate != null) return false;
} else if (!releaseDate.equals(other.releaseDate)) return false;
if (title == null) {
if (other.title != null) return false;
} else if (!title.equals(other.title)) return false;
return true;
}
}
package com.vogella.unittest.model;
public enum Alignment {
SUPER_EVIL, EVIL, NEUTRAL, GOOD, SUPER_GOOD;
}
package com.vogella.unittest.model;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Magical {
}
package com.vogella.unittest.model;
/**
* Race in Tolkien's Lord of the Rings.
*
* @author Florent Biville
*/
public enum Race {
HOBBIT("Hobbit", false, Alignment.GOOD), MAIA("Maia", true, Alignment.GOOD), MAN("Man", false, Alignment.NEUTRAL), ELF("Elf", true, Alignment.GOOD), DWARF("Dwarf", false, Alignment.GOOD), ORC("Orc", false, Alignment.EVIL);
private final String name;
private final boolean immortal;
private Alignment alignment;
Race(String name, boolean immortal, Alignment alignment) {
this.name = name;
this.immortal = immortal;
this.alignment = alignment;
}
public String getName() {
return name;
}
public boolean isImmortal() {
return immortal;
}
public Alignment getAlignment() {
return alignment;
}
@Override
public String toString() {
return "Race [name=" + name + ", immortal=" + immortal + "]";
}
}
package com.vogella.unittest.model;
@Magical
public enum Ring {
oneRing, vilya, nenya, narya, dwarfRing, manRing;
}
package com.vogella.unittest.model;
public class TolkienCharacter {
// public to test extract on field
public int age;
private String name;
private Race race;
// not accessible field to test that field by field comparison does not use it
@SuppressWarnings("unused")
private long notAccessibleField = System.currentTimeMillis();
public TolkienCharacter(String name, int age, Race race) {
this.name = name;
this.age = age;
this.race = race;
}
public Race getRace() {
return race;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
if (age<0) {
throw new IllegalArgumentException("Age is not allowed to be smaller than zero");
}
this.age = age;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((race == null) ? 0 : race.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
TolkienCharacter other = (TolkienCharacter) obj;
if (age != other.age) return false;
if (name == null) {
if (other.name != null) return false;
} else if (!name.equals(other.name)) return false;
if (race == null) {
if (other.race != null) return false;
} else if (!race.equals(other.race)) return false;
return true;
}
@Override
public String toString() {
return name + " " + age + " years old " + race.getName();
}
}
Create the com.vogella.unittest.services
package and copy and paste the following classes on it.
package com.vogella.unittest.services;
import static com.vogella.unittest.model.Race.DWARF;
import static com.vogella.unittest.model.Race.ELF;
import static com.vogella.unittest.model.Race.HOBBIT;
import static com.vogella.unittest.model.Race.MAIA;
import static com.vogella.unittest.model.Race.MAN;
import static com.vogella.unittest.model.Race.ORC;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.vogella.unittest.model.Movie;
import com.vogella.unittest.model.Ring;
import com.vogella.unittest.model.TolkienCharacter;
/**
* Init data for unit test
*/
public class DataService {
static final String ERROR_MESSAGE_EXAMPLE_FOR_ASSERTION = "{} assertion : {}\n";
// Some of the Lord of the Rings characters :
final TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
final TolkienCharacter sam = new TolkienCharacter("Sam", 38, HOBBIT);
final TolkienCharacter merry = new TolkienCharacter("Merry", 36, HOBBIT);
final TolkienCharacter pippin = new TolkienCharacter("Pippin", 28, HOBBIT);
final TolkienCharacter gandalf = new TolkienCharacter("Gandalf", 2020, MAIA);
final TolkienCharacter gimli = new TolkienCharacter("Gimli", 139, DWARF);
final TolkienCharacter legolas = new TolkienCharacter("Legolas", 1000, ELF);
final TolkienCharacter aragorn = new TolkienCharacter("Aragorn", 87, MAN);
final TolkienCharacter boromir = new TolkienCharacter("Boromir", 37, MAN);
final TolkienCharacter sauron = new TolkienCharacter("Sauron", 50000, MAIA);
final TolkienCharacter galadriel = new TolkienCharacter("Galadriel", 3000, ELF);
final TolkienCharacter elrond = new TolkienCharacter("Elrond", 3000, ELF);
final TolkienCharacter guruk = new TolkienCharacter("Guruk", 20, ORC);
final Movie theFellowshipOfTheRing = new Movie("the fellowship of the Ring", new Date(), "178 min");
final Movie theTwoTowers = new Movie("the two Towers", new Date(), "179 min");
final Movie theReturnOfTheKing = new Movie("the Return of the King", new Date(), "201 min");
public List<TolkienCharacter> getFellowship() {
final List<TolkienCharacter> fellowshipOfTheRing = new ArrayList<>();
// let's do some team building :)
fellowshipOfTheRing.add(frodo);
fellowshipOfTheRing.add(sam);
fellowshipOfTheRing.add(merry);
fellowshipOfTheRing.add(pippin);
fellowshipOfTheRing.add(gandalf);
fellowshipOfTheRing.add(legolas);
fellowshipOfTheRing.add(gimli);
fellowshipOfTheRing.add(aragorn);
fellowshipOfTheRing.add(boromir);
return fellowshipOfTheRing;
}
public List<TolkienCharacter> getOrcsWithHobbitPrisoners() {
final List<TolkienCharacter> orcsWithHobbitPrisoners = new ArrayList<TolkienCharacter>();
orcsWithHobbitPrisoners.add(guruk);
orcsWithHobbitPrisoners.add(merry);
orcsWithHobbitPrisoners.add(pippin);
return orcsWithHobbitPrisoners;
}
public TolkienCharacter getFellowshipCharacter(String name) {
List<TolkienCharacter> list = getFellowship();
return list.stream().filter(s-> s.equals(name)).findFirst().get();
}
public Map<Ring, TolkienCharacter> getRingBearers() {
Map<Ring, TolkienCharacter> ringBearers = new HashMap<>();
// ring bearers
ringBearers.put(Ring.nenya, galadriel);
ringBearers.put(Ring.narya, gandalf);
ringBearers.put(Ring.vilya, elrond);
ringBearers.put(Ring.oneRing, frodo);
return ringBearers;
}
}
7.3. Write tests for the model and the services
Create the following test class.
package com.vogella.unittest.services;
import static com.vogella.unittest.model.Race.HOBBIT;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import com.vogella.unittest.model.TolkienCharacter;
import com.vogella.unittest.services.DataService;
class DataServiceTest {
// TODO initialize before each test
DataService dataService;
@Test
void ensureThatInitializationOfTolkeinCharactorsWorks() {
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
// TODO check that age is 33
// TODO check that name is "Frodo"
// TODO check that name is not "Frodon"
fail("not yet implemented");
}
@Test
void ensureThatEqualsWorksForCharaters() {
Object jake = new TolkienCharacter("Jake", 43, HOBBIT);
Object sameJake = jake;
Object jakeClone = new TolkienCharacter("Jake", 12, HOBBIT);
// TODO check that:
// jake is equal to sameJake
// jake is not equal to jakeClone
fail("not yet implemented");
}
@Test
void checkInheritance() {
TolkienCharacter tolkienCharacter = dataService.getFellowship().get(0);
// TODO check that tolkienCharacter.getClass is not a movie class
fail("not yet implemented");
}
@Test
void ensureFellowShipCharacterAccessByNameReturnsNullForUnknownCharacter() {
// TODO imlement a check that dataService.getFellowshipCharacter returns null for an
// unknow felllow, e.g. "Lars"
fail("not yet implemented");
}
@Test
void ensureFellowShipCharacterAccessByNameWorksGivenCorrectNameIsGiven() {
// TODO imlement a check that dataService.getFellowshipCharacter returns a fellow for an
// existing felllow, e.g. "Frodo"
fail("not yet implemented");
}
@Test
void ensureThatFrodoAndGandalfArePartOfTheFellowsip() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
// TODO check that Frodo and Gandalf are part of the fellowship
fail("not yet implemented");
}
@Test
void ensureThatOneRingBearerIsPartOfTheFellowship() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
// TODO test that at least one ring bearer is part of the fellowship
fail("not yet implemented");
}
// TODO Use @RepeatedTest(int) to execute this test 1000 times
@Test
@Tag("slow")
@DisplayName("Minimal stress testing: run this test 1000 times to ")
void ensureThatWeCanRetrieveFellowshipMultipleTimes() {
dataService = new DataService();
assertNotNull(dataService.getFellowship());
fail("this should run 1000 times");
}
@Test
void ensureOrdering() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
// ensure that the order of the fellowship is:
// frodo, sam, merry,pippin, gandalf,legolas,gimli,aragorn,boromir
fail("not yet implemented");
}
@Test
void ensureAge() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
// TODO test ensure that all hobbits and men are younger than 100 years
// TODO also ensure that the elfs, dwars the maia are all older than 100 years
fail("not yet implemented");
// HINT fellowship.stream might be useful here
}
@Test
void ensureThatFellowsStayASmallGroup() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
// TODO Write a test to get the 20 element from the fellowship throws an
// IndexOutOfBoundsException
fail("not yet implemented");
}
}
Solve the TODO and ensure that all tests can be successfully executed from your IDE. You may find issues in the DataService with these tests, fix them if you encounter them.
Solution
The following is a possible implementation of the tests:
package com.vogella.unittest.services;
import static com.vogella.unittest.model.Race.HOBBIT;
import static com.vogella.unittest.model.Race.MAIA;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import com.vogella.unittest.model.Movie;
import com.vogella.unittest.model.Race;
import com.vogella.unittest.model.Ring;
import com.vogella.unittest.model.TolkienCharacter;
import com.vogella.unittest.services.DataService;
class DataServiceTest {
DataService dataService;
@BeforeEach
void setup() {
dataService = new DataService();
}
@Test
void ensureThatInitializationOfTolkeinCharactorsWorks() {
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
assertEquals(33, frodo.age, "Frodo should be 33");
assertEquals("Frodo", frodo.getName(), "Frodos character has wrong name");
assertNotEquals("Frodon", frodo.getName(), "Frodos character has wrong name");
}
@Test
void ensureFellowShipCharacterAccessByNameReturnsNullForUnknownCharacter() {
TolkienCharacter fellowshipCharacter = dataService.getFellowshipCharacter("Lars");
assertNull(fellowshipCharacter);
}
@Test
void ensureFellowShipCharacterAccessByNameWorksGivenCorrectNameIsGiven() {
TolkienCharacter fellowshipCharacter = dataService.getFellowshipCharacter("Frodo");
assertNotNull(fellowshipCharacter);
}
@Test
void ensureThatEqualsWorksForCharaters() {
Object jake = new TolkienCharacter("Jake", 43, HOBBIT);
Object sameJake = jake;
Object jakeClone = new TolkienCharacter("Jake", 12, HOBBIT);
assertEquals(jake, sameJake);
assertNotEquals(jake, jakeClone);
}
@Test
void checkInheritance() {
TolkienCharacter tolkienCharacter = dataService.getFellowship().get(0);
assertFalse(Movie.class.isAssignableFrom(tolkienCharacter.getClass()));
assertTrue(TolkienCharacter.class.isAssignableFrom(tolkienCharacter.getClass()));
}
@Test
void ensureThatFrodoAndGandalfArePartOfTheFellowsip() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
TolkienCharacter gandalf = new TolkienCharacter("Gandalf", 2020, MAIA);
assertTrue(fellowship.contains(frodo));
assertTrue(fellowship.contains(gandalf));
}
@Test
void ensureThatOneRingBearerIsPartOfTheFellowship() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
Map<Ring, TolkienCharacter> ringBearers = dataService.getRingBearers();
assertTrue(ringBearers.values().stream().anyMatch(ringBearer -> fellowship.contains(ringBearer)));
}
@RepeatedTest(1000)
@Tag("slow")
@DisplayName("Ensure that we can call getFellowShip multiple times")
void ensureThatWeCanRetrieveFellowshipMultipleTimes() {
dataService = new DataService();
assertNotNull(dataService.getFellowship());
}
@Test
void ensureOrdering() {
// ensure that the order of the fellowship is:
// frodo, sam, merry,pippin, gandalf,legolas,gimli,aragorn,boromir
List<TolkienCharacter> fellowship = dataService.getFellowship();
assertEquals(dataService.getFellowshipCharacter("Frodo"), fellowship.get(0));
assertEquals(dataService.getFellowshipCharacter("Sam"), fellowship.get(1));
assertEquals(dataService.getFellowshipCharacter("Merry"), fellowship.get(2));
assertEquals(dataService.getFellowshipCharacter("Pippin"), fellowship.get(3));
assertEquals(dataService.getFellowshipCharacter("Gandalf"), fellowship.get(4));
assertEquals(dataService.getFellowshipCharacter("Legolas"), fellowship.get(5));
assertEquals(dataService.getFellowshipCharacter("Gimli"), fellowship.get(6));
assertEquals(dataService.getFellowshipCharacter("Aragorn"), fellowship.get(7));
assertEquals(dataService.getFellowshipCharacter("Boromir"), fellowship.get(8));
}
@Test
void ensureAge() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
// test to ensure that all hobbits and men are younger than 100 years
// also ensure that the elfs, dwars the maia are all older than 100 years
assertTrue(fellowship.stream()
.filter(fellow -> fellow.getRace().equals(HOBBIT) || fellow.getRace().equals(Race.MAN))
.allMatch(fellow -> fellow.age < 100));
assertTrue(
fellowship
.stream().filter(fellow -> fellow.getRace().equals(Race.ELF)
|| fellow.getRace().equals(Race.DWARF) || fellow.getRace().equals(Race.MAIA))
.allMatch(fellow -> fellow.age > 100));
}
@Test
void ensureThatFellowsStayASmallGroup() {
List<TolkienCharacter> fellowship = dataService.getFellowship();
assertThrows(IndexOutOfBoundsException.class, () -> fellowship.get(20));
}
}
You actually found errors in the DataService
implementations, adjust the following method:
public TolkienCharacter getFellowshipCharacter(String name) {
List<TolkienCharacter> list = getFellowship();
return list.stream().filter(s -> s.getName().equals(name)).findFirst().orElseGet(() -> null);
}
7.4. Verify on command line
Verify that your code compiles and your test are running via the command line with:
-
mvn clean verify
in case you are using Maven -
./gradlew test
in case you are using Gradle
7.5. Add a long running method to your data service
Add a fake update method to your DataService
which takes a long time to update the data and returns true on success.
public boolean update() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
}
7.6. Develop a test to constrain the execution time of the long running method
Create a new test method in your DataServiceTest
.
Use the assertTimeout
assert statement to ensure that this test does not run longer than 3 seconds.
Solution
@Test
public void ensureServiceDoesNotRunToLong() {
assertTimeout(Duration.ofSeconds(3),()-> dataService.update());
}
8. Exercise: Develop unit tests for a regular expression utility method for email verification
8.1. Create the data model used for testing
Create the com.vogella.unittest.email
package and copy and paste the following classes on it.
package com.vogella.unittest.email;
import java.util.regex.Pattern;
public class EmailValidator {
/**
* Email validation pattern.
*/
public static final Pattern EMAIL_PATTERN = Pattern.compile(
"[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
"\\@" +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
"(" +
"\\." +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
")+"
);
private boolean mIsValid = false;
public boolean isValid() {
return mIsValid;
}
/**
* Validates if the given input is a valid email address.
*
* @param emailPattern The {@link Pattern} used to validate the given email.
* @param email The email to validate.
* @return {@code true} if the input is a valid email. {@code false} otherwise.
*/
public static boolean isValidEmail(CharSequence email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
}
8.2. Write tests for the model and the services
Create the following test class.
package com.vogella.unittest.email;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class EmailValidatorTest {
// TODO Write test for EmailValidator
// The names of the methods should give you a pointer what to test for
@Test
public void ensureThatEmailValidatorReturnsTrueForValidEmail() {
assertTrue(EmailValidator.isValidEmail("lars.vogel@gmail.com"));
}
@Test
@DisplayName("Ensure that the usage of a subdomain is still valid, see https://en.wikipedia.org/wiki/Subdomain")
public void emailValidator_CorrectEmailSubDomain_ReturnsTrue() {
fail("Fixme");
}
@Test
@DisplayName("Ensure that a missiong top level domain returns false")
public void emailValidator_InvalidEmailNoTld_ReturnsFalse() {
fail("Fixme");
}
@Test
public void emailValidator_InvalidEmailDoubleDot_ReturnsFalse() {
fail("Fixme");
}
@Test
public void emailValidator_InvalidEmailNoUsername_ReturnsFalse() {
fail("Fixme");
}
@Test
public void emailValidator_EmptyString_ReturnsFalse() {
fail("Fixme");
}
@Test
public void emailValidator_NullEmail_ReturnsFalse() {
fail("Fixme");
}
}
Fix all the failing test, unfortunately the test specification is not very good. Try to write reasonable tests which fit the method name.
8.3. Verify
Run your new test via the IDE. Verify that your code compiles and your test are running via the command line.
8.4. Solution
The following listing contains a possible implementation of the test.
Solution
package com.vogella.unittest.email;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class EmailValidatorTest {
@Test
void ensureThatEmailValidatorReturnsTrueForValidEmail() {
assertTrue(EmailValidator.isValidEmail("lars.vogel@gmail.com"));
}
@Test
void emailValidator_CorrectEmailSubDomain_ReturnsTrue() {
assertTrue(EmailValidator.isValidEmail("lars.vogel@analytics.gmail.com"));
}
@Test
void emailValidator_InvalidEmailNoTld_ReturnsFalse() {
assertFalse(EmailValidator.isValidEmail("lars.vogel@gmail"));
}
@Test
void emailValidator_InvalidEmailDoubleDot_ReturnsFalse() {
assertTrue(EmailValidator.isValidEmail("lars..vogel@gmail.com"));
assertFalse(EmailValidator.isValidEmail("lars..vogel@gmail..com"));
}
@Test
void emailValidator_InvalidEmailNoUsername_ReturnsFalse() {
assertFalse(EmailValidator.isValidEmail("@gmail.com"));
}
@Test
void emailValidator_EmptyString_ReturnsFalse() {
assertFalse(EmailValidator.isValidEmail(""));
}
@Test
void emailValidator_NullEmail_ReturnsFalse() {
assertFalse(EmailValidator.isValidEmail(null));
}
}
9. Exercise: Testing exceptions and conditional enablement
9.1. Write tests checking for exceptions
We also want to check that exceptions with the correct error messages are thrown, if we call the class under test with incorrect data.
Create the following test class.
package com.vogella.unittest.services;
import static com.vogella.unittest.model.Race.HOBBIT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.vogella.unittest.model.TolkienCharacter;
public class DataModelAssertThrowsTest {
@Test
@DisplayName("Ensure that access to the fellowship throws exception outside the valid range")
void exceptionTesting() {
DataService dataService = new DataService();
Throwable exception = assertThrows(IndexOutOfBoundsException.class, () -> dataService.getFellowship().get(20));
assertEquals("Index 20 out of bounds for length 9", exception.getMessage());
}
@Test
@Disabled("Please fix and enable")
public void ensureThatAgeMustBeLargerThanZeroViaSetter() {
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
// use assertThrows() rule to check that the message is:
// Age is not allowed to be smaller than zero
frodo.setAge(-1);
}
@Test
@Disabled("Please fix and enable")
public void testThatAgeMustBeLargerThanZeroViaConstructor() {
// use assertThrows() rule to check that an IllegalArgumentException exception is thrown and
// that the message is:
// "Age is not allowed to be smaller than zero"
TolkienCharacter frodo = new TolkienCharacter("Frodo", -1, HOBBIT);
}
}
Fix the disabled tests and enable them. The name should give a good indication what you have to do test here.
You may discover that the data model does not behave a expected by the test, fix them in this case.
9.2. Verify
Run your update test via the IDE.
Verify that your code compiles and your test are running via the command line with the mvn clean verify
.
9.3. Solution
Solution
package com.vogella.unittest.services;
import static com.vogella.unittest.model.Race.HOBBIT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.vogella.unittest.model.TolkienCharacter;
class DataModelAssertThrowsTest {
@Test
@DisplayName("Ensure that access to the fellowship throws exception outside the valid range")
void ensureThatIndexOutOfBoundMessageForFellowAccessIsCorrect() {
DataService dataService = new DataService();
List<TolkienCharacter> fellowship = dataService.getFellowship();
Throwable exception = assertThrows(IndexOutOfBoundsException.class, () -> fellowship.get(20));
assertEquals("Index 20 out of bounds for length 9", exception.getMessage());
}
@Test
void ensureThatAgeMustBeLargerThanZeroViaSetter() {
// Age is not allowed to be smaller than zero
TolkienCharacter frodo = new TolkienCharacter("Frodo", 33, HOBBIT);
assertThrows(IllegalArgumentException.class, () -> frodo.setAge(-1));
}
@Test
void ensureThatAgeMustBeLargerThanZeroViaConstructor() {
// Age is not allowed to be smaller than zero
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> new TolkienCharacter("Frodo", -1, HOBBIT));
assertEquals("Age is not allowed to be smaller than zero", exception.getMessage());
}
}
The test indicates that you need to update the TolkienCharacter
constructor.
public TolkienCharacter(String name, int age, Race race) {
this.name = name;
setAge(age);
this.race = race;
}
9.4. Enable test only on certain platforms
Write this test and adjust it so that is only runs on the operating system you are using.
package com.vogella.unittest.platform;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
class LinuxTests {
@Test
void testName() throws Exception {
// only run on Linux
Assumptions.assumeTrue(System.getProperty("os.name").contains("Linux"));
assertTrue(true);
}
}
10. Exercise: Writing nested tests to group tests for display
10.1. Write tests
Create the following test.
package com.vogella.unittest.nested;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class UsingNestedTests {
private List<String> list;
@BeforeEach
void setup() {
list = Arrays.asList("JUnit 5", "Mockito");
}
@Test
void listTests() {
assertEquals(2, list.size());
}
// TODO define inner class with @Nestled
// write one tests named checkFirstElement() to check that the first list element is "JUnit 4"
// write one tests named checkSecondElement() to check that the first list element is "JUnit 4"
@DisplayName("Grouped tests for checking members")
@Nested
class CheckMembers {
@Test
void checkFirstElement() {
assertEquals(("JUnit 5"), list.get(0));
}
@Test
void checkSecondElement() {
assertEquals(("Mockito"), list.get(1));
}
}
}
10.2. Solution
The following listing contains a possible implementation of the test.
Solution
package com.vogella.unittest.nested;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class UsingNestedTests {
private List<String> list;
@BeforeEach
void setup() {
list = Arrays.asList("JUnit 5", "Mockito");
}
@Test
void listTests() {
assertEquals(2, list.size());
}
}
10.3. Run tests
Run the test from your IDE and review how the grouped tests are displayed.
11. Exercise: Testing multiple parameter
11.1. Create class for testing
Create the com.vogella.unittest.converter
package and copy and paste the following class on it.
package com.vogella.unittest.converter;
public class ConverterUtil {
// converts to celsius
public static float convertFahrenheitToCelsius(float fahrenheit) {
return ((fahrenheit - 32) * 5 / 9);
}
// converts to fahrenheit
public static float convertCelsiusToFahrenheit(float celsius) {
return ((celsius * 9) / 5) + 32;
}
}
11.2. Write a dynamic test
Create the following test class.
package com.vogella.unittest.converter;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
class ConverterUtilTest {
int[][] celsiusFahrenheitMapping = new int[][] { { 10, 50 }, { 40, 104 }, { 0, 32 } };
@TestFactory
Stream<DynamicTest> ensureThatCelsiumConvertsToFahrenheit() {
return Arrays.stream(celsiusFahrenheitMapping).map(entry -> {
// access celcius and fahrenheit from entry
int celsius = entry[0];
int fahrenheit = entry[1];
return null;
// return a dynamicTest which checks that that the convertion from celcius to
// fahrenheit is correct
});
}
Stream<DynamicTest> ensureThatFahrenheitToCelsiumConverts() {
return null;
// TODO Write a similar test fahrenheit to celsius
}
}
Fix all the failing test, unfortunately the test specification is not very good. Try to write reasonable tests which fit the method name.
Show Solution
package com.vogella.unittest.converter;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
class ConverterUtilTest {
@TestFactory
public Stream<DynamicTest> ensureThatCelsiumConvertsToFahrenheit() {
ConverterUtil converter = new ConverterUtil();
int[][] data = new int[][] { { 10,50 }, { 40, 104}, { 0, 32 } };
return Arrays.stream(data).map(entry -> {
int celsius = entry[0];
int fahrenheit = entry[1];
return dynamicTest(celsius + " Celsius are " + fahrenheit, () -> {
assertEquals(fahrenheit, converter.convertCelsiusToFahrenheit(celsius));
});
});
}
@TestFactory
public Stream<DynamicTest> ensureThatFahrenheitConvertsToCelsius() {
ConverterUtil converter = new ConverterUtil();
int[][] data = new int[][] { { 10,50 }, { 40, 104}, { 0, 32 } };
return Arrays.stream(data).map(entry -> {
int celsius = entry[0];
int fahrenheit = entry[1];
return dynamicTest(celsius + " Celsius are " + fahrenheit, () -> {
assertEquals(celsius, converter.convertFahrenheitToCelsius(fahrenheit));
});
});
}
}
11.3. Verify
Run your new test via the IDE and ensure that you have 6 tests running succesfull.y
Verify that your code compiles and your test are running via the command line either with ./gradlew test`or with the `mvn clean verify
depending on your build system.
12. Exercise: Testing with multiple input parameter
Dynamic tests are included in the regular JUnit 5 library, which you already included. To use parameters in your tests, you have to add the junit-jupiter-params library.
12.1. Add dependency
If you are using Maven add the following dependency to junit-jupiter-params
to your Maven pom file.
org.junit.jupiter
junit-jupiter-params
5.7.2
test
If you are using Gradle add the following to your build.gradle file
implementation 'org.junit.jupiter:junit-jupiter-params:5.7.2'
12.2. Write a parameterized test
Review the following code:
package com.vogella.unittest.converter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class ParameterizedExampleTest {
static int[][] data() {
return new int[][] { { 1 , 2, 2 }, { 5, 3, 15 }, { 121, 4, 484 } };
}
@ParameterizedTest(name = "{index} called with: {0}")
@MethodSource(value = "data")
void testWithStringParameter(int[] data) {
MyClass tester = new MyClass();
int m1 = data[0];
int m2 = data[1];
int expected = data[2];
assertEquals(expected, tester.multiply(m1, m2));
}
// class to be tested
class MyClass {
public int multiply(int i, int j) {
return i * j;
}
}
}
Create a new test method in ConverterUtilTest which also uses a parameterized test.
12.3. Verify
Run your new test via the IDE.
Verify that your code compiles and your test are running via the command line with the ./gradlew test
or mvn clean verify
command based on your build system.
12.4. Add more options
ParameterizedTest
are very flexible in sense of their input.
The following lists a few more.
Add these to your test and run the tests again.
@ParameterizedTest
@ValueSource(strings = { "WINDOW", "Microsoft Windows [Version 10.?]" })
void ensureWindowsStringContainWindow(String name) {
assertTrue(name.toLowerCase().contains("window"));
}
@DisplayName("A negative value for year is not supported by the leap year computation.")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = { -1, -4 })
void ensureYear(int year) {
assertTrue(year < 0);
}
@ParameterizedTest(name = "{0} * {1} = {2}")
@CsvSource({ "0, 1, 0", "1, 2, 2", "49, 50, 2450", "1, 100, 100" })
void add(int first, int second, int expectedResult) {
MyClass calculator = new MyClass();
assertEquals(expectedResult, calculator.multiply(first, second),
() -> first + " * " + second + " should equal " + expectedResult);
}
13. Exercise: Using the @TempDir annotation to create temporary files and paths
In this exercise you learn how to use the @TempDir
annotation to let JUnit 5 create files and paths on request in your test and to automatically remove them after the test.
Java 11 API for creating files: |
13.1. Create class under test
Create the following class
package com.vogella.unittest.file;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileWriter {
private FileWriter() {
}
public static void createFile(Path path) {
try {
Files.write(path, "".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void appendFile(Path path, String content) throws IOException {
// image more logic here...
Files.writeString(path, content, StandardOpenOption.APPEND);
}
}
13.2. Write tests
Using the @TempDir annotation, create unit which test named FileWriterTest
for the following:
-
Ensure that the Path given to you by the @TempDir annotation if writable
-
Ensure that a appending to a file with FileWriter.appendFile which has not yet been created with FileWriter.createFile throws an exception
-
Ensure that you can write to the file once you created it
HINT:
@Test
void ensureThatPathFromTempDirISWritable(@TempDir Path path) {
// Check if the path created by the TempDir extension is writable
// Check `Files` API for this
}
13.2.1. Solution
Solution
package com.vogella.unittest.file;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class FileWriterTest {
@Test
void ensureThatPathFromTempDirISWritable(@TempDir Path path) {
// Check if the path created by the TempDir extension is writable
assertTrue(Files.isWritable(path));
}
@Test
void ensureThatNonExistingFileThrowsAnException(@TempDir Path path) {
Path file = path.resolve("content.txt");
assertThrows(IOException.class, () -> {
FileWriter.appendFile(file, "Hello");
});
}
@Test
void ensureAppendingWorks(@TempDir Path path) throws IOException {
Path file = path.resolve("content.txt");
FileWriter.createFile(file);
FileWriter.appendFile(file, "Hello");
assertTrue(Files.isReadable(file));
// TODO check the content of the file
}
}
14. Exercise: Testing for annotations
In this exercise you write test to check class under test for certain annotations.
14.1. Add dependency to @Inject
If you have not yet done this, add a dependency to javax.inject.
Maven:
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
Gradle:
implementation 'javax.inject:javax.inject:1'
14.2. Create class under test
Create the following class
package com.vogella.unittest.di;
import jakarta.inject.Inject;
public class Service {
@Inject
String s;
@Inject
public Service() {
}
@Inject
public Service(String s) {
this.s = s;
}
}
t validates that the Servic === Write tests
Write a test that validates that the Service class only has one constructor annotated with @Inject
.
HINT:
-
The class has a `getConstructors method.
-
The
Constructor
has a method getAnnotation
14.3. Solution
Solution
package com.vogella.unittest.di;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.Constructor;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
class ServiceTest {
@Test
void ensureJSR330Constructor() {
int count = 0;
Constructor<?>[] constructors = Service.class.getConstructors();
for (Constructor<?> constructor : constructors) {
Inject annotation = constructor.getAnnotation(Inject.class);
if (annotation != null) {
count++;
}
}
assertEquals(1, count);
}
}
15. Exercise: Create test reports
Both Maven and Gradle allow to generate HTML report for unit tests.
Gradle creates these automatically, if you run the ./gradlew build
command and with Maven
you run the mvn clean verify surefire-report:report
command.
Run this for your project and check the build folder for the generated test reports.
16. Exercise: Clone the JUnit5 Github repo and review tests
Open JUnit5 Github page in your browser and clone the repo.
Import the project into your favorite IDE and review some of the tests, e.g. the platform-tests
contains a lot of useful tests.
17. Overview of JUnit5 annotations
The following table gives an overview of the most important annotations in JUnit 5 from the org.junit.jupiter.api
package.
Annotation | Description |
---|---|
|
Identifies a method as a test method. |
|
Disables a test method with an option reason. |
|
Executed before each test. Used to prepare the test environment, e.g., initialize the fields in the test class, configure the environment, etc. |
|
Executed after each test. Used to cleanup the test environment, e.g., delete temporary data, restore defaults, cleanup expensive memory structures. |
|
<Name> that will be displayed by the test runner. In contrast to method names the name can contain spaces to improve readability. |
|
Similar to |
|
Annotates a static method which is executed once, before the start of all tests.
It is used to perform time intensive activities, for example, to connect to a database. Methods marked with this annotation need to be defined as |
|
Annotates a static method which is executed once, after all tests have been finished.
It is used to perform clean-up activities, for example, to disconnect from a database. Methods annotated with this annotation need to be defined as |
|
Annotates a method which is a Factory for creating dynamic tests |
|
Lets you nest inner test classes to group your tests and to have additional @BeforeEach and @AfterEach methods. |
|
Tags a test method, tests in JUnit 5 can be filtered by tag. E.g., run only tests tagged with "fast". |
|
Lets you register an Extension class that adds functionality to the tests |
18. Conclusion
JUnit 5 makes is easy to write software tests.
The implementation of all these examples and code snippets can be found over on Github. The Maven examples are located in JUnit with Maven and the Gradle examples are located in JUnit 5 with Gradle.
If you need more assistance we offer Online Training and Onsite training as well as consulting