JUnit 5 by Examples

JUnit 5 by Examples

  • September 11, 2022
Table Of Contents

In software development, unit testing means writing tests to verify that our code is working as logical units.

Different people tend to think of a “logical unit” in different ways. We will define it as a unit of behavior that tests not a method or a class on its own but as the collective behavior of many methods that does useful business work.

Unit testing allows us to catch bugs early on in the development phase plus it allows us to refactor and change our code knowing that if we mess up something the tests will light red and notify us.

Example Code

This article is accompanied by a working code example on GitHub.

JUnit and JUnit 5

JUnit is an Open Source testing framework for Java that relies heavily on annotations to run and manage our tests. It allows us to write our tests in logically separated suites which are executed in parallel fast runs.

JUnit 5

JUnit 5 was developed to leverage the new and powerful advances in the Java language introduced in Java 8 and beyond. It embraces the functional and declarative style of programming which is more readable and easier to write. JUnit 5 can use more than one extension at a time, which was not possible in earlier versions where only one runner could be used at a time. This allows us to combine Spring extensions with other extensions including custom ones that we write.

Basic components of the JUnit 5 project

The components of JUnit are:

JUnit Platform

  • Serves as a foundation for launching testing frameworks on the JVM.
  • Provides the Test Engine API for developing a testing framework that runs on the platform.

JUnit Jupiter

  • Offers us the annotations and the extensions to write the tests.

JUnit Vintage

  • Offers us the ability to run JUnit 4 and JUnit 3 tests on the platform.

Annotations

JUnit annotations are offered to us by the Jupiter component. They make our tests much more readable and easy to write. Let’s take a look at the most important ones.

Before we start with the code we need to add the junit-jupiter-api dependency.

<dependencies>  
	 <dependency> 
		 <groupId>org.junit.jupiter</groupId>  
		 <artifactId>junit-jupiter-api</artifactId>  
		 <version>5.9.0</version>  
		 <scope>test</scope>  
	 </dependency>
 </dependencies>

@Test

Adding this annotation to a public void method allows us to simply run that method as a test.

import org.junit.jupiter.api.Test;  
import static org.junit.jupiter.api.Assertions.assertEquals;  
  
public class DogTest {  
  @Test  
  public void testBark() {  
        String expectedString = "woof";  
	    assertEquals(expectedString, "woof");  
  }  
}

Here we use the assertEquals() method to assert that the two strings are equal, we will cover it in more detail later. Now, to run the tests all we need to do is run the maven command mvn test.

We should see the test run and pass:

[INFO] Running DogTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.005 s - in DogTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Let’s check what happens when the test fails.

public class DogTest {  
	@Test  
	public void barkFailure() {  
	    String expectedString = "Meow";  
	    assertEquals(expectedString, "Woof");  
	}
}

Then we will see this in the log output:

[INFO] Running DogTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.066 s <<< FAILURE! - in DogTest
[ERROR] DogTest.barkFailure  Time elapsed: 0.036 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <Meow> but was:

@BeforeAll

A method annotated with @BeforeAll has to be a static method and it will run once before any test is run.

public class DogTest {  
  
  @BeforeAll  
  public static void init() {  
        System.out.println("Doing stuff");  
  }  
  @Test  
  public void testBark() {  
        String expectedString = "woof";  
		assertEquals(expectedString, "woof");  
	    System.out.println("WOOF!");  
  }  
}

This will output:

Doing stuff
WOOF!

@BeforeEach

A method annotated with @BeforeEach will run once before every test method annotated with @Test.

public class DogTest {  
	@BeforeEach  
	public void doEach() {  
	    System.out.println("Hey Doggo");  
	}  
	  
	@Test  
	public void testBark1() {  
	    String expectedString = "woof1";  
		assertEquals(expectedString, "woof1");  
	    System.out.println("WOOF => 1");  
	}  
	  
	@Test  
	public void testBark2() {  
	    String expectedString = "woof2";  
	    assertEquals(expectedString, "woof2");  
	    System.out.println("WOOF => 2");  
	}
}

This will output:

Hey Doggo
WOOF => 1
Hey Doggo
WOOF => 2

Order not guaranteed

JUnit doesn’t guarantee that tests will run in the order they are in the file, so we might see WOOF => 2 before WOOF => 1.

@AfterAll

Similar to @BeforeAll, but it runs after all the tests are run:

public class DogTest { 
	@AfterAll  
	public static void finish() {  
	    System.out.println("Finishing stuff");  
	}  
	  
	@Test  
	public void testBark1() {  
	    String expectedString = "woof1";  
	    assertEquals(expectedString, "woof1");  
	    System.out.println("WOOF => 1");  
	}
}

This should output:

WOOF => 1
Finishing stuff

@AfterEach

This will run once after each test is run:

public class DogTest { 
	@AfterEach  
	public void doAfterEach() {  
	    System.out.println("Bye Doggo");  
	}  
	  
	@Test  
	public void testBark1() {  
	    String expectedString = "woof1";  
	    assertEquals(expectedString, "woof1");  
	    System.out.println("WOOF => 1");  
	}  
	  
	@Test  
	public void testBark2() {  
	    String expectedString = "woof2";  
	    assertEquals(expectedString, "woof2");  
	    System.out.println("WOOF => 2");  
	}
}

This should output

WOOF => 1
Bye Doggo
WOOF => 2
Bye Doggo

@Disabled

This annotation can be applied to @Test methods to prevent them from running, or it can be even applied to a class to prevent all the @Test methods inside it from running.

public class DogTest { 

	@Disabled("Dog 1 please don't woof")  
	@Test  
	public void testBark1() {  
	    String expectedString = "woof1";  
	    assertEquals(expectedString, "woof1");  
	    System.out.println("WOOF => 1");  
	}  
	  
	@Test  
	public void testBark2() {  
	    String expectedString = "woof2";  
	    assertEquals(expectedString, "woof2");  
	    System.out.println("WOOF => 2");  
	}
}

We should see the test not running and the disabled message being displayed:

Dog 1 please don't woof
WOOF => 2

Assertions

JUnit offers basic assertions through the Jupiter module. Let’s explore the main ones. For the full list please check JUnit’s documentation.

You don’t have to use JUnit’s assertions, but can instead use a library like assertJ, if you want. Read more about assertJ in our dedicated article.

You can also use Hamcrest assertions as explained later in this article.

assertEquals()

This method compares two given parameters for equality. It can take any of the primitive types (int, float, etc…) plus accepting Objects for which it calls the equals() method to check the equality. We have seen many examples of it in our previous code snippets.

assertNotEquals()

It checks for non-equality for two given parameters (primitives or objects).

public class DogTest { 
	@Test  
	public void testNotBark() {  
	    String unexpectedString = "";  
	    assertNotEquals(unexpectedString, "woof");  
	    System.out.println("Didn't woof!!");  
	}
}

This should pass and print:

Didn't woof!!

assertNull()

It simply does a null check on the given parameter:

public class DogTest { 
	@Test  
	public void nullCheck() {  
	    Object dog = null;  
		assertNull(dog);  
	    System.out.println("Null dog :(");  
	}
}

This will print:

Null dog :(

assertNotNull()

Unlike, assertNul(), this fails if the given object isn’t null;

public class DogTest { 
	@Test  
	public void nonNullCheck() {  
	    String dog = "Max";  
	    assertNotNull(dog);  
	    System.out.println("Hey I am " + dog);  
	}
}

This will print:

Hey I am Max

assertTrue()

It will fail if the given boolean parameter is false:

public class DogTest { 
	@Test  
	public void trueCheck() {  
	    int dogAge = 2;  
	    assertTrue(dogAge < 5);  
	    System.out.println("I am young :)");  
	}
}

This will print:

I am young :)

assertFalse()

It will fail if the given boolean parameter is true:

public class DogTest { 
	@Test  
	public void falseCheck() {  
	    int dogAge = 7;  
	    assertFalse(dogAge < 5);  
	    System.out.println("I am old :(");  
	}
}

This should show us:

I am old :(

Assertions with Hamcrest

The first question that comes to mind is, why do we need an extra dependency to do the assertions when JUnit offers us a basic set of assertions?

Hamcrest is one of the most popular libraries for writing assertions, it offers a fluent API that enables us to easily write and read object matchers.

As we will see in the code examples below, Hamcrest assertions make the tests much more readable because they read almost like natural sentences. Let’s get to know some of the most important methods of Hamcrest.

First, let’s introduce the Maven dependency:

<dependency>  
	 <groupId>org.hamcrest</groupId>  
	 <artifactId>hamcrest-all</artifactId>  
	 <version>1.3</version>  
	 <scope>test</scope>  
</dependency>

assertThat()

This is the starting point for most of our assertion statements, as the first parameter it takes the object (or primitive) under test, and as the second parameter, it takes a Matcher:

import org.junit.jupiter.api.Test;  
import static org.hamcrest.MatcherAssert.assertThat;  
import static org.hamcrest.Matchers.*;  
  
public class CatTest {  
    @Test  
	public void testMeow() {  
        String catName = "Stilla";  
		int catAge = 3;  
		boolean isNice = false;  
  
	   assertThat(catName, equalTo("Stilla"));  
	   assertThat(catAge, lessThan(5));  
	   assertThat(isNice, is(false));  
  }  
}

Notice the flexibility we have on the Matchers we pass, there are many more Matchers we can use on many more data types.

Let’s explore those Matchers a bit more.

instanceOf()

It tests whether an object is an instance of a certain class. Say we have a Cat class

public class Cat {  
}
public class CatTest {  
	@Test  
	public void testCatInstance() {  
	    Cat cat = new Cat();  
	  
	    assertThat(cat, instanceOf(Cat.class));  
	}
}

sameInstance()

It verifies if two object references are the same instance.

public class CatTest {  
	@Test  
	public void testSameCatInstance() {  
	    Cat cat = new Cat();
	    assertThat(cat, sameInstance(cat));  
	}
}

hasItems()

It verifies that the given items exist in the collection under test. The existence is checked through the equals() object method.

public class CatTest {  
	@Test  
	public void testCollectionContaining() {  
	    List<String> catNames = asList("Phibi", "Monica", "Stilla");  
	  
	    assertThat(catNames, hasItems("Monica", "Phibi"));  
	    assertThat(catNames, not(hasItems("Melih")));  
	}
}

Note the chaining of calls performed with not(hasItems("Melih")), this allows us to mix and match those operators to have complex and readable assertions.

hasSize()

It checks the size of a given collection against an expected size.

public class CatTest {  
	@Test  
	public void testCollectionSize() {  
	    List<String> catNames = asList("Phibi", "Monica");  
	  
	    assertThat(catNames, hasSize(2));  
	}
}

hasProperty()

This is used to check if an object’s property (field) matches a certain condition.

Firstly, let’s modify our Cat class to have a name property with a getter and a constructor.

public class Cat {  
    private String name;  
  
    public Cat(String name) {  
        this.name = name;  
    }  
  
    public String getName() {  
        return name;  
    }
 }
public class CatTest {  
	@Test  
	public void testBean() {  
	    Cat cat = new Cat("Mimi");  
	  
	    assertThat(cat, hasProperty("name", equalTo("Mimi")));  
	}
}

equalToIgnoringCase()

It checks if two strings match regardless of capital or small letters.

public class CatTest {  
	@Test  
	public void testStringEquality() {  
	    String catNameInCaps = "RACHEL";  
	  
	    assertThat(catNameInCaps, equalToIgnoringCase("rachel"));  
	}
}

containsString()

It checks if a string is contained in another string.

public class CatTest {  
	@Test  
	public void testStringContains() {  
	    String catName = "Joey The Cute";  
	  
	    assertThat(catName, containsString("Cute"));  
	}
}

Assumptions

We can use Assumptions when we want a test to run only under certain conditions (i.e. under certain assumptions). If the condition isn’t met then JUnit simply ignores the test. This is different from using an assert(), for example, which will cause the test to fail if the condition isn’t met.

Say we have a test that we only want to run on Windows machines:

Let’s start by introducing a GoldFish class

public class GoldFish {
    private String name;
    private int age;
    // constructor and getters
}
@Test
public void testBooleanAssumption() {
        GoldFish goldFish = new GoldFish("Windows Jelly", 1);

        assumeTrue(System.getProperty("os.name").contains("Windows");
        assertThat(goldFish.getName(), equalToIgnoringCase("Windows Jelly"));
}

If we are running on a Linux machine we will see that the test is ignored with the following message:

org.opentest4j.TestAbortedException: Assumption failed: assumption is not true

Had we used the assert() like this:

@Test  
public void testBooleanAssert() {  
    GoldFish goldFish = new GoldFish("Windows Jelly", 1);  
  
    assert(System.getProperty("os.name").contains("Windows"));  
    assertThat(goldFish.getName(), equalToIgnoringCase("Windows Jelly"));  
}

our test would instead fail with an assertion error:

java.lang.AssertionError

Exception Testing

As part of our test scenarios, we might want to test that in certain conditions a particular exception is thrown. Let’s check out how JUnit 5 helps us do this.

JUnit 5 offers us the assertThrows() method, which takes the class of the exception we are excepting (or one of its superclasses) as a first parameter. As for the second parameter it expects a Runnable that contains the code under test.

Let’s add a method to calculate the speed of GoldFish:

public class GoldFish {  
    private String name;  
	private int age;
	
	public int calculateSpeed() {  
	    if (age == 0){  
	        throw new RuntimeException("This will fail :((");  
	  }  
	    return 10 / age;  
	}
	// constructor and getters
}

Our test would look like this:

public class GoldFishTest {
	@Test  
	public void testException() {  
	    GoldFish goldFish = new GoldFish("Goldy", 0);  
	  
	    RuntimeException exception = assertThrows(RuntimeException.class, goldFish::calculateSpeed);  
	  
	    assertThat(exception.getMessage(), equalToIgnoringCase("This will fail :(("));  
	}
}

Parameterized Testing

The idea behind parameterized testing is to execute the same test but with different parameters. JUnit 5 offers a rich API to handle this, however, we are going to briefly explore the most common way to do it.

We first need to add this dependency

<dependency>  
	 <groupId>org.junit.jupiter</groupId>  
	 <artifactId>junit-jupiter-params</artifactId>  
	 <version>5.9.0</version>  
	 <scope>test</scope>  
</dependency>
public class GoldFishTest {
	@ParameterizedTest  
	@MethodSource("provideFishes")  
	public void parameterizedTest(GoldFish goldFish) {  
	    assertTrue(goldFish.getAge() >= 1);  
	}  
	  
	private static Stream<Arguments> provideFishes() {  
	    return Stream.of(  
	            Arguments.of(new GoldFish("Browny", 1)),  
			    Arguments.of(new GoldFish("Greeny", 2))  
	    );  
	}
}

Notice that the name provided to the @MethodSource annotation should match the name of the provider method, provideFishes() in this case. This will run the same test twice but with two different parameter values.

Conclusion

We have explored JUnit 5 and its components, and we have also covered the most important test annotations. We learned how to use the Assertions API and assert thrown exceptions.

Written By:

Abdulcelil Cercenazi

Written By:

Abdulcelil Cercenazi

Software developer and craftsman. Loves working with Java | Spring | Docker. Always looking for new adventures

Recent Posts

Handling Timezones in a Spring Boot Application

It is common to encounter applications that run in different time zones. Handling date and time operations consistently across multiple layers of an application can be tricky.

Read more

Scheduling Jobs with Node.js

Have you ever wanted to perform a specific task on your application server at specific times without physically running them yourself?

Read more

Rollout Strategies with Feature Flags

Rolling out new features is one of the most satisfying parts of our job. Finally, users will see the new feature we’ve worked on so hard!

Read more