Icon for gfsd IntelliJ IDEA

How to write JUnit test cases? A step-by-step guide with examples

An illustration for part 1 of Symflower's Java unit test tutorial which takes you through the steps of how to write JUnit test cases and also contains Java examples

If you’re new to writing JUnit test cases, this 2-part guide will provide you with the solid fundamentals you’ll need to start writing unit tests using JUnit.

Unit testing with JUnit

As unit tests become mainstream, many frameworks that support unit testing for Java have gained more popularity. Java testing frameworks such as JUnit describe the guidelines for test scripts, and define the fundamental structure of tests as well as a strategy for your testing cycle. Among them, JUnit is the de facto standard framework for unit testing in Java applications.

Why use JUnit for unit testing Java applications?

JUnit enables you to efficiently write unit tests to find and fix bugs in your code early. Using JUnit for unit tests can accelerate development by discovering issues early and often, and can help you deliver higher-quality code faster. It is also often used for TDD, so if you’re aiming to try test-driven development, you’re likely to encounter JUnit and will need to master its use.

In this blog series you will learn everything you need to know for working with JUnit:

  • If you’re new to unit tests using JUnit, Part 1 of this tutorial will help you get started. It includes Java unit test examples, and provides best practices for writing JUnit test cases!
  • In Part 2: Advanced techniques, we take a deeper dive into some of the more advanced techniques of JUnit such as parameterized tests, assumptions, and timeouts.

🀼 JUnit vs TestNG

Read our post comparing popular Java unit testing frameworks JUnit and TestNG!

How to write unit tests for Java with JUnit?

Overall, the general process of unit testing is fairly simple. You’ll define inputs to explore selected paths in your software, and determine what output you expect to receive. If the expected value is returned, the test will pass, and you can go on coding. If it fails, you’ll need to fix your implementation to make sure your unit test passes.

The general process of unit testing

The knowledge around writing JUnit test cases isn’t rocket science, but you’ll need to understand the process and a few best practices can come in handy.

You’ll first set up a test method for each isolated component (usually a function or method in your code) that you’re testing. You’ll use annotations to identify test methods and to indicate when and how to execute these test methods. Each test method contains one or several assertions that are essentially static methods to compare expected and actual results. That’s how you check if the tested function of your application actually works as expected.

In this guide to writing JUnit tests for Java, we’ll be using JUnit 5 with Maven to guide you through the required steps. We’ll also provide examples, and best practices to boost the efficiency of your unit testing efforts!

How to create Java unit tests with JUnit 5 (and Maven)? A step-by-step guide

1) Setting up an empty Maven project

Since we want to write JUnit tests, we need to download and configure JUnit as a dependency for our project to build and execute tests. For these tasks we are using Maven.

Use the following command to create a basic project structure for your Maven project:

mvn archetype:generate -DgroupId=com.symflower.demo  -DartifactId=tdd -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=LATEST -DinteractiveMode=false

This will create the following directory structure in the project directory “tdd”, which we will use in our running example:

tdd
β”œβ”€β”€ pom.xml
└── src
   β”œβ”€β”€ main
   β”‚   └── java
   β”‚       └── com
   β”‚           └── symflower
   β”‚               └── demo
   β”‚                   └── App.java
   └── test
       └── java
           └── com
               └── symflower
                   └── demo
                       └── AppTest.java

You can go ahead and delete the two dummy files App.java and AppTest.java as we are going to add our own implementation and testing files in just a bit. However, keep the directories in any case, since they define where we put the implementing code and our tests.

Next, let’s edit the created pom.xml to include JUnit 5 for unit testing.

πŸ§‘β€πŸ’» Upgrading from JUnit 4 to 5

Ready to make the switch from JUnit 4 to 5 but don’t know where to begin? Here’s our step-by-step migration guide!

2) Adding JUnit to Maven

To add JUnit to Maven, add the following dependency tag in your pom.xml to be included in your dependencies tag. In case a JUnit dependency already exists, replace the old one since we want to write JUnit 5 tests. The following snippet shows you how your dependencies tag should look like when you are done:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.9.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Check out the available JUnit 5 versions if you want to use the latest release!

In order for Maven to actually find the tests to execute, you’ll also have to add the Maven Surefire Plugin to the build plugins tag in your pom.xml:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M3</version>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

Check out the available Maven Surefire Plugin versions if you want to use the latest release!

🐘 Using Gradle? No problem!

Check out our post about running JUnit tests with Gradle: How to run JUnit 5 tests with Gradle?

3) Creating the JUnit test file: where should your tests live?

You are now ready to write your first unit test! But where in your directory structure should your unit test reside?

Obey the Maven naming conventions to ensure Maven is able to find your tests. That is, postfix your test class with β€œTest” and put the test files under: src/test/<package-path-to-class-under-test>

In our running example, we are going to test a geometrical Triangle class which is able to represent a triangle and tell you if a valid triangle can be constructed with the provided sides a, b, and c.

The Triangle class is stored in: src/main/java/com/symflower/demo/triangle/Triangle.java Therefore, the test file needs to be put under: src/test/java/com/symflower/demo/triangle/TriangleTest.java

If you want to store your test files in a different directory, you can configure Maven by specifying the testSourceDirectory property in your POM file.

4) Creating your first JUnit test

That’s it for preparations, let’s get started with writing your first JUnit test case! In this example, we want to test a method of the Triangle class with the following signature:

public boolean isValid();

It returns true in case the triangle is a valid triangle, i.e. with the provided sides a, b and c a triangle can be constructed. Two sides combined need to be smaller or equal to the third, and each side needs to be longer than 0.

Our first JUnit 5 test checks if a triangle with a negative side is recognized as being invalid and looks as follows:

package com.symflower.demo.triangle;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class TriangleTest {
   @Test
   public void isValidNegativeSide() {
       Triangle t = new Triangle(1, 1, -1);
       boolean expected = false;
       boolean actual = t.isValid();

       assertEquals(expected, actual);
   }
}

First, the test class needs to import the org.junit.jupiter.api package as well as the assertions class (org.junit.jupiter.api.Assertions).

The test method isValidNegativeSide has the typical structure of first initializing the object under test t; declaring the expected result ‘expected’, calling the method under test, and finally using an assertion to ensure that the actual behavior matches the expected one.

JUnit 5 offers a wide variety of assertion methods to check the actual behavior. Make sure to check out the JUnit 5 documentation to learn about the available methods!

Try Symflower in your IDE to generate unit test templates & test suites

5) Running your JUnit tests

If all your JUnit 5 dependencies are added and Maven is configured, it’s time to run your tests and analyze the results! With JUnit, you can choose to run your test from your IDE, right from the command line, or using your preferred build system (Maven or Gradle). For the purposes of this article, we’ll stick with using Maven on the command line, which makes it very simple to run JUnit tests. Specifically, you’ll be using either of the following methods. If you want to run the entire test suite, use:

mvn test

Want to run tests from a specific test class only? Use:

mvn test -Dtest="TriangleTest"

The output for a passing test run in the command line looks as follows:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.symflower.demo.triangle.TriangleTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.087 s - in com.symflower.demo.triangle.TriangleTest
[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
------------------------------------------------------------------------
[INFO] BUILD SUCCESS
------------------------------------------------------------------------

It shows that a single test case was executed, which passed. It took 87 ms to run. The output of a failing test run on the other hand may look as follows:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.symflower.demo.triangle.TriangleTest
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.047 s <<< FAILURE! - in com.symflower.demo.triangle.TriangleTest
[ERROR] isValidNegativeSide  Time elapsed: 0.039 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <false> but was: <true>
        at com.symflower.demo.triangle.TriangleTest.isValidNegativeSide(TriangleTest.java:13)
[INFO] Results:
[ERROR] Failures:
[ERROR]   TriangleTest.isValidNegativeSide:13 expected: <false> but was: <true>
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
------------------------------------------------------------------------
[INFO] BUILD FAILURE
------------------------------------------------------------------------

We executed a single test case in TriangleTest resulting in a failing test case. The respective error message tells us that an assertion in line 13 in the test method isValidNegativeSide failed in the test class TriangleTest. Make sure you check out our unit testing best practices later on in this post to learn how to produce more meaningful error messages than just expected: <false> but was: <true>.

How to filter JUnit tests using tags?

With JUnit 5, you can categorize test cases, and execute groups of unit tests together. The typical use case of that is to specify which tests are unit tests, integration, or system tests, and have them executed at different stages of the CI. Another use case is to specify in which environment a test should be executed in, e.g. only in development or in both testing and production.

The @Tag annotation is used to group test cases together and filter for relevant test cases. Adding a tag is as simple as:

@Tag("unittest")
@Test
public void isValidNegativeSide() {
    <...testing logic...>
}

When using Maven in the command line, filter for tests that are tagged with unittest as follows:

mvn test -Dgroups="unittest"

Best practices for writing JUnit tests

There are a few best practices for writing JUnit test cases that will help make sure that your unit tests are accurately written, efficient, and simple. This makes it easier to make sense of your tests and production code for others and your future self πŸ˜‰.

1) Keep your unit tests as small as possible

A unit test should always test a single behavior of a function and not several use cases. This way, it is easier to pinpoint a failing test to its underlying bug as it is crystal clear which aspect of the function under test is broken.

For our running example, we would introduce separate test cases for all sorts of invalid triangles rather than testing all invalid triangles within a single test method.

2) Supply messages to assert methods

Using assertions with an error message supplied is a general best practice. This way, failing test cases provide more information and are typically faster to understand for other people looking at your code.

Sticking with our example of the TriangleTest, when this unit test fails, the default error message would simply look like:

[ERROR]   TriangleTest.isValidNegativeSide:13 expected: <false> but was: <true>

That doesn’t give much of a clue as to what is actually happening. By supplying a message like: assertEquals(expected, actual, "A triangle with a negative side is invalid.");, the emitted error message gives the debugging engineer a better overview of what is going on:

[ERROR]   TriangleTest.isValidNegativeSide:13 A triangle with a negative side is invalid. ==> expected: <false> but was: <true>

3) Use the best-fitting assert method and adhere to the argument order

When looking at the JUnit 5 documentation, you will note that the first parameter to all assert* methods is the expected value. Always make sure to pass the expected value as the first parameter! Otherwise, you’ll just end up with confusing error messages for all those failing unit tests.

Another best practice is to always use the best-fitting assert method. That is, rather than using assertEquals(false, actual); you should actually go with: assertFalse(actual).

Let’s apply these best practices to our first unit test example from above:

@Test
public void isValidNegativeSide() {
    Triangle t = new Triangle(1, 1, -1);
    boolean actual = t.isValid();

    assertFalse(actual, "A triangle with a negative side is invalid.");
}

4) Reduce code duplication by using @BeforeAll, @BeforeEach, @AfterAll, @AfterEach

JUnit offers several annotations that allow you to specify if code should be executed before or after the methods within a test class are run. If you find yourself always typing the same setup and tearing down code, it might be time to take a look at those annotations!

In addition to reducing code duplication, making use of these facilities will also increase the speed of your unit tests. It is, for instance, faster to set up an in-memory DB once for a test class rather than for every method call.

Find the full list of supported annotations in the JUnit Documentation.

So let’s take a look at a unit test that makes use of these annotations:

package com.symflower.demo.triangle;

import org.junit.jupiter.api.*;
import com.symflower.demo.database.*;;

public class TriangleManagerTest {
   static Database InMemoryDB;

   @BeforeAll
   public static void setUpDB() {
       System.out.println("@BeforeAll is called once.");
       InMemoryDB = Database.getInMemoryDB("TriangleManagerTest");

       // More setup code for the in memory database
   }

   @AfterAll
   public static void tearDownDB() {
       System.out.println("@AfterAll is called once.");
       Database.closeInMemoryDB("TriangleManagerTest");
   }

   @AfterEach
   public void clearTriangleTable() {
       System.out.println("@AfterEach is called after each test method.");
       InMemoryDB.clearTable("Triangle");
   }

   @Test
   public void updateTriangleExisting() {
       System.out.println("Test method updateTriangleExisting is executed once");
       TriangleManager t2 = new TriangleManager(InMemoryDB);

       // More test code
   }

   @Test
   public void updateTriangleNotExisting() {
       System.out.println("Test method updateTriangleNotExisting is executed once");
       TriangleManager t2 = new TriangleManager(InMemoryDB);

       // More test code
   }
}

The TriangleManagerTest class requires a database to be set up. Rather than setting it up separately for every test method and tearing it down again, this is only done once by using the annotations @BeforeAll and @AfterAll. Note that the methods annotated with @BeforeAll and @AfterAll need to be defined as static!

Use the annotations @BeforeEachand @AfterEach for code that needs to be executed before or after each test method, respectively. Executing the tests of the above test class leads to the following output:

@BeforeAll is called once.
The test method updateTriangleExisting is executed once
@AfterEach is called after each test method.
The test method updateTriangleNotExisting is executed once
@AfterEach is called after each test method.
@AfterAll is called once.
Please note that the outputs to System.out were only added to showcase the execution order of the methods in TriangleManagerTest.

Simplifying unit test creation with Symflower

To streamline your testing workflow, Symflower can automagically write smart unit test templates for your code. With just a click, Symflower will automatically generate boilerplate code for your unit tests, and place it in your test file. All you need to do is fill in relevant values and your tests are ready to run. Better still, in some cases, it can also deliver ready-to-compile unit tests with meaningful values! Check out Symflower in action in the video below, or install the free tool right away:

Conclusion: a JUnit testing tutorial for unit testing Java code

So there you have it! If you’re new to JUnit, now you know how to write unit tests for Java, run JUnit test cases, and use key best practices to make sure those tests are efficient and accurate for nice, clean, and bug-free code.

Don’t miss part 2 of this article series on how to write JUnit test cases which covers more advanced techniques like using parameterized tests, assumptions in JUnit 5, and making sure a method does not exceed a defined timeout. Subscribe to our newsletter to be notified once that article is published! Follow us on Twitter, LinkedIn or Facebook for more insightful content around unit testing and software development in general.

| 2023-02-09