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

This is part 1 of our guide to unit testing with JUnit which covers the basics with Java unit test examples and best practices for writing JUnit test cases.

💡 A series on JUnit testing

  • Part 2: Advanced techniques takes a deeper dive into some of the more advanced techniques of JUnit such as parameterized tests, assumptions, and timeouts.
Automatically migrate JUnit 4 to JUnit 5.

Table of contents:

Java unit testing frameworks and JUnit

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 software faster. It is also often used for TDD, so if you’re aiming to try test-driven development in a Java environment, you’re likely to encounter JUnit and will need to master its use.

💡 Why write unit tests when you can generate them?

Unit testing doesn’t need to be all manual work. Symflower lets you automatically generate tests for your code. The IDE plugin plugin integrates into your workflow and generates smart test templates, complete test suites, and provides test-backed diagnostics for your applications.

Download for VS Code, IntelliJ, or try Symflower in the CLI.

How to write unit tests for Java with JUnit?

When unit testing applications, 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.

To start writing JUnit test cases, 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 improve the efficiency of your unit testing workflow.

🐘 Using Gradle? No problem!

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

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.

Automatically migrate JUnit 4 to JUnit 5.

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.

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

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

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 browse through the JUnit 5 documentation to learn about the available methods.

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.

To run the entire test suite, use:

mvn test

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 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

A few best practices for writing JUnit test cases help create accurately written, efficient, and simple unit tests. 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. 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.

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.

Automated unit test generation with Symflower

Symflower is an IDE plugin and CLI tool that lets you automatically create both smart unit test templates and entire test suites. Among other features, the tool provides real-time code diagnostics based on the generated tests right in your IDE, automated linting and code repair, and test impact analysis to speed up test execution times.

Try Symflower in VS Code, IntelliJ, or the CLI, and see Symflower's documentation for more info. Here’s a quick TDD example with Symflower:

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.

Automatically migrate JUnit 4 to JUnit 5.

Subscribe to our newsletter, follow us on X, LinkedIn, and Facebook for more insightful content about unit testing and software development in general.

Try Symflower in your IDE to generate unit test templates & test suites
| 2023-02-09