How to Write Effective Unit Tests in Java

March 07, 2022
Written by
Ryan Kay
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

header - How to Write Effective Unit Tests in Java

In the early years of my career, getting started with testing my code felt like trying to break through a brick wall. To hopefully make your experience less painful, here is an attempt at the introduction I was given to this important topic.

In this article, you will learn how to write code which is easily testable and how to test it using a single tool: JUnit 5.

Prerequisites

  • Java Development Kit (JDK) version 8 or newer.
  • JUnit 5.8.2 (though most versions of JUnit 5 should work).
  • An IDE with JUnit integration is recommended, such as Eclipse, Intellij IDEA, Android Studio, NetBeans. This article will make references to IntelliJ IDEA.
  • A build tool is recommended, such as Gradle or Maven to manage the JUnit 5 dependency.

Why You Should Care About Testing

Anytime you write application code that is deployed and used, it is being tested. Whether you write tests in code, manually test it via a browser or device, or publish it and let your users test it, is merely a detail. I do not recommend that last option!

Most start by manual testing which means to deploy and use the application like a typical user would. While there is a time for that, checking critical application logic can be done in a systematic, faster, and more reliable way by writing code tests.

What seems like extra work initially, will save you time for two reasons: Firstly, you will catch and identify your errors sooner. Secondly you can run your tests with lightning speed on your local JVM environment.

Building and deploying your application to a server or a device for manual testing can be orders of magnitude slower by comparison.

How To Write Testable Code

Testing your code means verifying that it works as intended. However, getting from there to writing code tests might be challenging. To make matters worse, many tutorials go into great detail about a particular tool without describing the basic testing process.

To avoid any confusion, the first example will demonstrate how to test some code without using any libraries at all; not even JUnit. With that being said, I do encourage you to find a setup that works for you.

NOTE: Some of you may have heard of test driven development (TDD). I will not be applying TDD in these examples, but I will address it and the topic of 100% code coverage, toward the end of this article.

A Basic Process For Testing

For this example, you may either use a JDK via command line, along with a basic text editor to write the code, or use your preferred Java IDE (recommended). I will be using Intellij IDEA Community Edition to write and run the code.

Create a new Java Gradle project in IntelliJ IDEA by selecting the menu options File -> New -> Project. For this example, Gradle is not necessary, but later examples will benefit from having some kind of build tool ready.

New project creation wizard in Intellij IDEA

The Code To Be Tested

Suppose you are writing a user facing application where the user must create a username. In this situation, it is often necessary to write validation logic which places restrictions on what characters the user may choose for their username.

Create a new Java class called Validator.java inside the src/main/java package and copy and paste the following code:

import java.util.regex.Pattern;

public class Validator {

    /*
    The regex below matches against:
    - a-z characters of the alphabet lower case
    - A-Z characters of the alphabet upper case
    - 0-9 numbers
     */
    private static final String regex = "^[a-zA-Z0-9]+$";
    
    /**
     * A Valid name is:
     * - between 6 and 12 characters long
     * - Alphanumeric: letters and numbers only
     *
     * @param name Username to be validated against
     * @return Whether username is valid or not
     */
    public static boolean validateUserName(String name) {
        if (name.length() < 6 || name.length() > 12) return false;
        //initializes the regex pattern, gives it the username, and returns true or false if they match
        return Pattern.compile(regex).matcher(name).matches();
    }

    //...
}

Regex is short for “Regular Expression” and it can be thought of as a set of instructions used to examine the contents of String objects. Although it can be difficult to read, using a regex pattern is often much faster than writing the tedious validation logic in pure Java.

What Is A Unit Test?

Now that you have something to test, this is a good time to introduce a term which you may have heard: "Unit Test". I agree with a common definition that a "Unit" can be thought of as “the smallest testable part of an application.” Keep in mind that you will find other definitions from other developers.

The validateUserName(...) method can be understood as a unit of code. It is a single method call with a single output. It makes use of some libraries such as String.length(),  but it is not your responsibility to test the standard library itself.

Later on, you will see an example of a Java class that contains multiple dependencies, and is responsible for coordinating the events and flow of data between them. There will remain only a single method call on this class for each Unit Test. However, that single method may need to call many other methods within the same class, or other classes, to serve its functionality.

One of the core ideas of Object Oriented Programming is Data Hiding which means that in Java, testing private member (class) methods directly is difficult. There are some workarounds to this problem, but I find it sufficient to regard private member methods which are subsequently called from a public method to be part of the whole unit of code.

The Code Which Does The Testing

Now that you have some context of the sample code, it's time to test it. The process of unit testing code can be summarized in three steps:

  1. Prepare test inputs to the code (if any are required).
  2. Call the code (unit) to be tested.
  3. Verify what it does (behavior) and the result it returns.

This can be done by creating a class which contains a main method.

Create a class called ValidatorTests.java within the same subfolder:

public class ValidatorTests {
    private static final String TEST_ONE = "Valid short username";
    private static final String TEST_TWO = "Valid long username";
    private static final String TEST_THREE = "Invalid short username";
    private static final String TEST_FOUR = "Invalid long username";
    private static final String TEST_FIVE = "Empty username";

    private static final String SUCCESSFUL = " was successful";
    private static final String FAILED = " has failed";

    public static void main(String[] args) {
        testValidNames();
        testInvalidNames();
    }

    /**
     * Success only if true (valid) is returned
     */
    private static void testValidNames() {
        if(Validator.validateUserName("ryankay")) testLogger(TEST_ONE, SUCCESSFUL);
        else testLogger(TEST_ONE, FAILED);

        if(Validator.validateUserName("ryankay12345")) testLogger(TEST_TWO, SUCCESSFUL);
        else testLogger(TEST_TWO, FAILED);
    }

    /**
     * Success only if false (invalid) is returned
     */
    private static void testInvalidNames() {
        if(!Validator.validateUserName("ryank@y")) testLogger(TEST_THREE, SUCCESSFUL);
        else testLogger(TEST_THREE, FAILED);

        if(!Validator.validateUserName("ryankay12#456")) testLogger(TEST_FOUR, SUCCESSFUL);
        else testLogger(TEST_FOUR, FAILED);

        if(!Validator.validateUserName("")) testLogger(TEST_FIVE, SUCCESSFUL);
        else testLogger(TEST_FIVE, FAILED);
    }

    private static void testLogger(String testName, String result) {
        System.out.println("Test " + testName + result);
    }
}

NOTE: I have deliberately left out a particular test which would test a common situation which will crash an Java application; any idea what it might be?

Depending on what you are testing, a good start is to test one or two valid and invalid inputs. In this case, the way in which you observe the results of each test, is by printing the result.

In Intellij IDEA, the quickest way to run your code is to locate the class that contains the main method. Then, click the green arrow beside the class declaration or the main method itself:

How to run a main method in Intellij IDEA

After running this code, the console output should look similar to the image below:

The console outputs a series of messages indicating the each of the tests were successful

Everything seems to be working, but it has occurred to me that Java’s famous Billion Dollar Mistake has not yet been accounted for! Add the highlighted code to the ValidatorTests class:

public class ValidatorTests {
    private static final String TEST_ONE = "Valid short username";
    private static final String TEST_TWO = "Valid long username";
    private static final String TEST_THREE = "Invalid short username";
    private static final String TEST_FOUR = "Invalid long username";
    private static final String TEST_FIVE = "Empty  username";
    private static final String TEST_SIX = "Null username";

//…

    /**
     * Success only if false (invalid) is returned
     */
    private static void testInvalidNames() {
        if(!Validator.validateUserName("ryank@y")) testLogger(TEST_THREE, SUCCESSFUL);
        else testLogger(TEST_THREE, FAILED);

        if(!Validator.validateUserName("ryankay12#456")) testLogger(TEST_FOUR, SUCCESSFUL);
        else testLogger(TEST_FOUR, FAILED);

        if(!Validator.validateUserName("")) testLogger(TEST_FIVE, SUCCESSFUL);
        else testLogger(TEST_FIVE, FAILED);

        if(!Validator.validateUserName(null)) testLogger(TEST_SIX, SUCCESSFUL);
        else testLogger(TEST_SIX, FAILED);
    }

    private static void testLogger(String testName, String result) {
        System.out.println("Test " + testName + result);
    }
}

With the new test added, the console output is now:

Failing test output

Yikes! This can be fixed by adding a null check to the validateUserName(...) method defined in the Validator.java file:

    public static boolean validateUserName(String name) {
        if (name == null) return false;
        if (name.length() < 6 || name.length() > 12) return false;
        // initializes our regex pattern, gives it the username, and returns true or false if they match
        return Pattern.compile(regex).matcher(name).matches();
    }

In this example we only checked the output of the code. In the next section, I will explain how to verify the behavior of the code.

You also saw an example of a core OOP principle in testing: Do not assume that adequate test coverage can be achieved by adding a single test to a unit; though on occasion this may be true. Over time, you will get a sense of what kind of tests will be required in a given unit.

Here are a few tips to think about:

  • If Arrays or List indexes are involved, test the smallest and largest allowable indexes to catch ArrayIndexOutOfBoundsException.
  • If there is any possibility that a reference type (non-primitive type) may be null, test for it.
  • If large numbers and mathematics are involved, consider testing around the maximum size of Java’s primitives number types.

The point is, pure standard library code makes testing seem less stressful than it should be.

The Key To Testable Code: Decision Makers and Doers

This next idea has been given several names by different software engineers. I will restate it using names which were chosen for clarity of understanding.

This section discusses the option of testing with a variety of platform APIs and 3rd party frameworks and libraries. This is a common situation; particularly in projects which require a graphical user interface.

To make a useful generalization, the key to making your code testable is to separate it into two kinds of software entities such as classes or methods.

The first is what I will call a “Doer.” The job of a doer is to talk to some platform or 3rd party library/framework code and hide it from the rest of the application code.

Doers should have basic methods such as setColor(...)showMessage(...), or saveData(...) and may report success or failure if appropriate. They should contain as little logic as possible, for reasons that will become clear in a moment.

The second kind is what I like to call a "Decision Maker." Decision makers are broadly meant to accomplish two tasks:

  1. Encapsulate (contain) the various kinds of logical instructions which you will come across in most kinds of applications.
  2. Coordinate the flow of data and events to and between one or more doers (explained soon).

Perhaps more importantly is what they must not do:

  1. They must try not to contain dependencies to platform APIs or 3rd party libraries/frameworks.
  2. They must not make (instantiate) the doers that they depend on.
  3. Optionally, they should not know or depend on concrete class names.

We will observe a practical example of a GUI application. This same principle can apply regardless of whether you are writing Android, Java EE, Java Desktop applications, and most languages and platforms.

Preliminary Classes For A Typical GUI Application

At this point, you will likely want to have a project that is using some kind of build tool like Gradle or Maven. However, it is possible to integrate dependencies like JUnit 5 without them.

If at any point you want clarification about source code or directory structure for the following examples, visit the source code repository here.

Most Java applications will use some kind of Plain Old Java Object to represent core objects and their properties. The following examples represent a generic “task management” application, which will require such an object.

Make a new subdirectory called task in the same project directory where you created Validator and ValidatorTests.

Create a Java class called Task.java within the task directory and copy the following code to the file:

package task;

public class Task {
    private final int taskId;
    private final String taskName;
    private final String taskIcon;
    private final String taskColor;

    public Task(int taskId, String taskName, String taskIcon, String taskColor) {
        this.taskId = taskId;
        this.taskName = taskName;
        this.taskIcon = taskIcon;
        this.taskColor = taskColor;
    }

    public int getTaskId() {
        return taskId;
    }
    public String getTaskName() {
        return taskName;
    }
    public String getTaskIcon() {
        return taskIcon;
    }
    public String getTaskColor() {
        return taskColor;
    }
}

All of the user’s tasks will be contained in a single object, defined in another file named Tasks.java. Apart from acting as a collection of Task.java objects, it has a convenient method to look up a particular task by its taskId.

Create a Java class called Tasks.java within the task directory and copy the following code to the file:

package task;

public class Tasks {
    private final Task[] tasks;

    public Tasks(Task[] tasks) {
        this.tasks = tasks;
    }

    public Task[] get() {
        return tasks;
    }

    public Task getTaskById(int taskId) {
        for (Task task : tasks) {
            if (task.getTaskId() == taskId) return task;
        }

        return null;
    }
}

Another common and useful class to create in a GUI application is one which represents user interaction events in a particular screen. You can create such a class using the following pattern:

  1. Create an enum class to represent each event.
  2. Create a class which wraps the enum class.
  3. Give the “wrapper class” a variable to represent the event, along with any data associated with it.

Within the task subdirectory, create a new Java class called ManageTaskEvent.java with the following code:

package task; 

public class ManageTaskEvent {
    private final Event event;
    private final Object value;

    public ManageTaskEvent(Event event, Object value){
        this.event = event;
        this.value = value;
    }

    public Event getEvent() {
        return event;
    }
    public Object getValue() {
        return value;
    }

    public enum Event { ON_COLOR_SELECTED, ON_DONE_CLICK, ON_ICON_SELECTED, ON_START }
}

The Decision Maker

In the task subdirectory, create a new class called ManageTaskDecisionMaker.java. Replace the contents with the following code:

package task; 

public class ManageTaskDecisionMaker {
    private IManageTaskContract.View view;
    private IManageTaskContract.ViewModel vm;
    private ITaskStorage storage;

    public ManageTaskDecisionMaker(IManageTaskContract.View view,
                                   IManageTaskContract.ViewModel vm,
                                   ITaskStorage storage) {
        this.view = view;
        this.vm = vm;
        this.storage = storage;
    }

    public void onViewEvent(ManageTaskEvent event) {
        switch (event.getEvent()) {
            case ON_START:
                onStart();
                break;
            case ON_COLOR_SELECTED:
                onColorSelected((String) event.getValue());
                break;
            case ON_DONE_CLICK:
                updateStorage();
                break;
            case ON_ICON_SELECTED:
                onIconSelected((String) event.getValue());
                break;
        }
    }

    private void onStart() {
        storage.getTask(vm.getTask().getTaskId(), new Continuation<Task>() {
            @Override
            public void onSuccess(Task result) {
                vm.setTask(result);
                view.setColor(result.getTaskColor());
                view.setName(result.getTaskName());
                view.setIcon(result.getTaskIcon());
            }

            @Override
            public void onException(Exception e) {
                view.showMessage(e.getMessage());
                view.goToTaskListScreen();
            }
        });
    }

    private void onIconSelected(String icon) {
        Task oldTask = vm.getTask();
        Task update = new Task(
                oldTask.getTaskId(),
                oldTask.getTaskName(),
                icon,
                oldTask.getTaskColor()
        );

        vm.setTask(update);
        view.setIcon(update.getTaskIcon());
    }

    private void updateStorage() {
        Task oldTask = vm.getTask();
        Task update = new Task(
                oldTask.getTaskId(),
                view.getName(),
                oldTask.getTaskIcon(),
                oldTask.getTaskColor()
        );

        storage.updateTask(update, new Continuation<Void>() {
            @Override
            public void onSuccess(Void result) {
                view.goToTaskListScreen();
            }

            @Override
            public void onException(Exception e) {
                view.showMessage(e.getMessage());
                view.goToTaskListScreen();
            }
        });
    }

    private void onColorSelected(String color) {
        Task oldTask = vm.getTask();
        Task newTask = new Task(
                oldTask.getTaskId(),
                oldTask.getTaskName(),
                oldTask.getTaskIcon(),
                color
        );
        vm.setTask(newTask);
        view.setColor(newTask.getTaskColor());
    }
}

The general point I want to make first is that this article is not about teaching any particular GUI architecture pattern. All of them encourage this separation of decision makers and doers to some degree, and none of them are ideal for every kind of project or feature.


There is no such thing as a perfect software architecture; they just tend to work better or worse depending on project requirements.

Prefer Standard Library Only

It is recommended for decision makers to only contain code made from, or depending on, the Java Standard Library (jstdlib). This is because every dependency potentially adds overhead complexity to the tests.

Take for example, the popular Reactive Extension library, RxJava 2. Some developers love to use this library to glue their entire software architecture together. Supposing the decision maker used RxJava 2, the unit tests must now:

  1. Become tightly coupled to the RxJava 2 library.
  2. Work with the RxJava 2 API within the tests.

Though not necessarily a critique of RxJava 2 specifically, the common pitfalls of tight coupling to such a library are:

  • The learning curve required to actually test such code.
  • A limited amount, or complete lack of integration for testing.
  • The complete destruction of test code if you migrate to a different library.
  • Code breaking API changes over time.

Conversely, standard library code will tend to change much slower than platform and 3rd party library code. This insulates your decision makers and the test code from breaking changes.

It is unrealistic to expect that every line of code you write, particularly in a GUI application will be standard library only. Therefore, the next two sections will explore how to use many such libraries while avoiding the tight coupling problem.

Dependency Injection

Dependency Injection (DI) is a topic which can be difficult to grasp. However, it is important to understand its basic forms to increase the testability of code.

Our decision maker does not instantiate its own dependencies. This is fundamentally what dependency injection is about: separating configuration from use; to paraphrase Martin Fowler.

Instead of creating its dependencies, these dependencies are passed (injected) to it via a constructor method:

public class ManageTaskDecisionMaker {
    private IManageTaskContract.View view;
    private IManageTaskContract.ViewModel vm;
    private ITaskStorage storage;

    public ManageTaskDecisionMaker(IManageTaskContract.View view,
                                   IManageTaskContract.ViewModel vm,
                                   ITaskStorage storage) {
        this.view = view;
        this.vm = vm;
        this.storage = storage;
    }
//…
}

The usefulness of dependency injection becomes evident as you begin to test classes like the decision maker. Later on, you will create Test Doubles for the dependencies, and pass them into the decision maker.

Thanks to dependency injection, all of this can be done without changing any code in the decision maker or the Unit Tests. dependency injection effectively makes it such that the decision maker does not care whether it talks to fake dependencies or real ones.

Instead, if you instantiated the dependencies within the decision maker, you would need to change the decision maker’s source code to reference Test Doubles or the production classes whenever you want to run tests; and vice versa.

Dependency injection is useful, but it turns out that it can be used with another technique to make coupling even looser.

Consider Depending On Abstractions

Abstractions allow you to convey only information which is actually useful; leaving out details which are not.

Interfaces, abstract classes, and inheritance are all forms of abstraction as applied in code. This is not the only function they serve, but a primary one. All of the dependencies for the decision maker are interfaces, instead of concrete classes.

Create a new file named IManageTaskContract.java in the same project directory and paste the following code to create a new interface:

package task;

public interface IManageTaskContract {
    interface View {
        void setName(String name);
        String getName();
        void setIcon(String icon);
        void setColor(String c);
        void goToTaskListScreen();
        void showMessage(String message);
    }

    interface ViewModel {
        void setTask(Task task);
        Task getTask();
    }
}

As you can see, it is possible to nest interfaces inside of one another. If you have more than one UI feature, the top level interface works like a namespace.

Java interfaces allow you to define abstract methods, like Task getTask();. A Java method, excluding optional keyword modifiers, consists of two integral parts: a method signature and a method body.

The method signature consists of a return type, the method’s name, and an optional parameter list. The method body consists of the curly brackets, {//…}, along everything between them, that follows the method signature.

It is common to refer to the method signature as behavior of a method, and the method body as the implementation of the method.

The utility of using interfaces is that the decision maker will neither know or care if the actual dependency behind the interface is a Test Double or production code.

Create ITaskStorage.java in the same project directory, and paste the following code into the file:

package task;

public interface ITaskStorage {
    public void getTasks(Continuation<Tasks> continuation);

    public void getTask(int taskId, Continuation<Task> continuation);

    public void updateTask(Task task, Continuation<Void> continuation);
}

The second interface represents some kind of Input-Output (IO) device. It could be a database, network adapter (REST, GraphQL, etc.), repository, system service, and so on. Again, the point is that the decision maker does not know what it is actually talking to, only how to talk to it.

Create Continuation.java in the same project directory, and paste the following code into the file:

package task;

public interface Continuation<T> {
    public void onSuccess(T result);
    public void onException(Exception e);
}

The third interface contains another useful tool in the Java language: Generic Types. This is another kind of abstraction. The actual types are specified as necessary in the ITaskStorage.java interface, which is then used by the decision maker.

You may be wondering what the purpose of the Continuation interface is. When talking to an IO device of some kind, there is often a chance that it will throw an exception. The Continuation interface is an alternative to using a try-catch block to manage exceptions in a more idiomatic way.

A Note On Using Abstractions

Most developers, when they first learn about abstractions, will tend to use them everywhere.

Here are some tips on when to use abstractions:

  • You expect the implementation of some dependency to change over time.
  • You have multiple implementations (i.e. test and production) that you expect to use frequently.
  • You want to build and test classes such as the decision maker without having to build the dependencies first.
  • You do not like to use mocking frameworks (explained later).

There are other ways to get around swapping implementations for test and production code apart from using interfaces. However, most of these workarounds require a framework to mock the dependencies, or a build tool configured to supply different source sets.

I will not be implementing ITaskStorage, ViewModel, or View in this article as that is a platform specific detail. As an exercise, you may wish to implement them yourself in a toy application of your preferred platform.

How To Test Code With JUnit 5

Before writing the tests for the decision maker, you need to be able to use the JUnit 5 library in the test sources.

The JUnit 5 Documentation provides some examples of how to integrate the library into different build tools.

A Glance At The Test Source Set

A new Intellij IDEA Java project should come with a Test source set, which should sit in the same folder as your main source set:

Intellij IDEA project explorer

This is the folder which will keep everything that is used in the Unit Tests. While the code within the test source set can reference and use the code within the main source set, the same is not true in reverse. Furthermore, the test sources will not be present in the compiled application code outside of running in a testing environment.

Importing JUnit 5 With Gradle

If you created a Gradle Java project initially, then you will have a build.gradle file which sits in the root directory of your project. It should already have some import statements for JUnit 4. While JUnit 4 is still commonly used, please replace those dependencies with JUnit 5.

Open build.gradle and change the dependencies script to the following:

dependencies {
// note: you may need to update the version number; see the JUnit docs for more info
    testImplementation(platform('org.junit:junit-bom:5.8.2'))
    testImplementation('org.junit.jupiter:junit-jupiter')
}

Afterwards, you will need to synchronize your project dependencies. This can be done in Intellij by selecting the following menu items: Build -> Rebuild Project

Create The Test Data

As previously discussed, a fundamental piece of testing code is to create test inputs for the units to be tested. Since the test inputs in this article will be used across different test cases, it will be handy to have a file that contains all of the test data.

Within the src/test set, locate the java subfolder and create TestData.java:

import task.Task;
import task.Tasks;

public class TestData {
    static final int TASK_ID = 123456;
    static final String TASK_NAME = "Work";
    static final String TASK_ICON = "work_icon.png";
    static final String TASK_COLOR = "DARK_BLUE";

    public static Task getTestTask() {
        return new Task(TASK_ID, TASK_NAME, TASK_ICON, TASK_COLOR);
    }

    public static Tasks getTestTasks() {
        return new Tasks(
                new Task[]{
                        getTestTask()
                }
        );
    }
}

Create Test Doubles

There exists an esoteric set of names to describe various kinds of approaches to testing a production class or method with code that is used exclusively for testing (often broadly called Test Doubles). The unfortunate truth is that developers do not agree on what each of these terms mean.

Therefore I will call one approach as creating “Fakes” and the other approach as creating “Mocks”. Yes, you will find people who disagree with my usage. Welcome to the joy of software developers arguing about definitions.

A mock can be thought of as a class created by a "Mocking Framework." This means that the developer programmatically gives the class names, methods, and desired responses to the framework within the test sources.

Then at compile time or runtime, the framework will work some magic to provide a Test Double with the same concrete name. Therefore, a Mock does not need to be hidden behind an interface.

I used to use mocking libraries all of the time in my Unit Tests, and I know several senior developers who still do. The reason I no longer use them is because I find they suffer from the same issue that almost every framework does.

It will tend to work very well in many situations, but it will also be a major nuisance in a few special cases. With that being said, I do encourage you to explore them. A good mocking framework can help you write most of your tests a bit faster.

Create The “Fakes”

In this section, you will use what is commonly referred to as either a Fake or Stub. You will create some Java classes which implement the interfaces that are referenced in the decision maker. The benefit of this approach is that you have absolute control over these Test Doubles and no extra frameworks are needed to use them.

The downside is that they often take a bit more time to write as opposed to using a mocking library. Often, but not always.

Again, within the test source set java folder, create FakeView.java and paste the following code into the file:

import task.IManageTaskContract;

class FakeView implements IManageTaskContract.View {

    boolean setNameCalled = false;
    boolean getNameCalled = false;
    boolean setIconCalled = false;
    boolean setButtonColorCalled = false;
    boolean goToTaskListScreenCalled = false;
    boolean showMessageCalled = false;

    @Override
    public void setName(String name) {
        setNameCalled = true;
    }

    @Override
    public String getName() {
        getNameCalled = true;
        return "Lorem Ipsum";
    }

    @Override
    public void setIcon(String icon) {
        setIconCalled = true;
    }

    @Override
    public void setColor(String c) {
        setButtonColorCalled = true;
    }

    @Override
    public void goToTaskListScreen() {
        goToTaskListScreenCalled = true;
    }

    @Override
    public void showMessage(String message) {
        showMessageCalled = true;
    }
}

In this case, you are checking whether or not a given method was called. Details such as how many times the method is called, what it is called with, or what it should return, can all be written into the Fake as needed.

Next, create FakeViewModel.java in the same test subfolder:

import task.IManageTaskContract;
import task.Task;

class FakeViewModel implements IManageTaskContract.ViewModel {
    private Task task = null;
    boolean setTaskCalled = false;
    boolean getTaskCalled = false;

    @Override
    public void setTask(Task task) {
        this.task = task;
        setTaskCalled = true;
    }

    @Override
    public Task getTask() {
        getTaskCalled = true;
        return task;
    }
}

You may notice that I have initialized the task reference to be null. In the real application which this code is based on, task is initially loaded from storage. If  getTask() is called prematurely, it will cause the test to fail; which is the intended behavior in this case.

Lastly, create FakeTaskStorage.java in the same test subfolder:

import task.Continuation;
Import task.ITaskStorage;
import task.Task;
import task.Tasks;

import java.io.IOException;

class FakeTaskStorage implements ITaskStorage {
    private Boolean willFail = false;

    public void setWillFail(Boolean willFail) { this.willFail = willFail; }

    @Override
    public void getTasks(Continuation<Tasks> continuation) {
        if (willFail) continuation.onException(new IOException());
        else continuation.onSuccess(TestData.getTestTasks());
    }

    @Override
    public void getTask(int taskId, Continuation<Task> continuation) {
        if (willFail) continuation.onException(new IOException());
        else continuation.onSuccess(TestData.getTestTask());
    }

    @Override
    public void updateTask(Task task, Continuation<Void> continuation) {
        if (willFail) continuation.onException(new IOException());
        else continuation.onSuccess(null);
    }
}

As discussed previously, IO devices often throw exceptions. This Fake contains a boolean which can alter in the Unit Tests depending on if you want it to signal success or failure.

How To Write Unit Tests With JUnit 5

You are now ready to write some tests!

In the test source set java folder, create a class called ManageTaskDmTests.java:

import task.ManageTaskDecisionMaker;
import task.ManageTaskEvent;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

public class ManageTaskDmTests {
    private FakeView view;
    private FakeViewModel vm;
    private FakeTaskStorage storage;
    private ManageTaskDecisionMaker dm;
//…
}

For each Unit Test you write, you will write a method in this class. I typically have one test class per Java class to be tested but that is not strictly required.

How To Set Up Your Tests

Before writing any tests, you will make use of the JUnit 5 annotation @BeforeEach. A method marked with this annotation, will be executed prior to each Unit Test. This ensures that the changes made to the decision maker and the Fakes in one test do not affect the outcomes of other tests.

Add the setup() method to ManageTaskDmTests.java below the variable declarations:

//…
@BeforeEach
    void setup(){
        view = new FakeView();
        vm = new FakeViewModel();
        vm.setTask(TestData.getTestTask());
        vm.setTaskCalled = false;
        storage = new FakeTaskStorage();
        dm = new ManageTaskDecisionMaker(view, vm, storage);
    }


@Test
    public void onColorSelected() {
        ManageTaskEvent event = new ManageTaskEvent(
                ManageTaskEvent.Event.ON_COLOR_SELECTED,
                TestData.TASK_COLOR
        );

        dm.onViewEvent(event);

        assertTrue(view.setButtonColorCalled);
        assertTrue(vm.setTaskCalled);
    }

Here, you can see another JUnit 5 annotation: @Test. This annotation will be used by compilers and test runners  to create an executable test. If you are using a modern IDE, these steps should be handled for you.

This leads me to an important step before running these tests! By default, the tests will be “run” by gradle. While this option can work, many users (myself included) report that it does not without further configuration.

The simplest fix is to run your tests using the Intellij IDEA IDE itself. This setting can be changed by navigating through the following menu options:

  • On Windows OS, navigate to File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle
  • On Mac OS, navigate to Intellij IDEA -> Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle

Once you reach the “Gradle menu, change the “Run tests using:” dropdown menu to “Intellij IDEA”:

Intellij Test runner settings

Click on the Apply button at the bottom, then OK.

How To Run A Unit Test

Before running the tests, you may wish to review the full source code of this example.

You will now need a run configuration which points to either your test class to run all the tests in one go, or to a specific test within your test class. To do so, click on the green play arrow next to either the class declaration or the test method declaration as seen in the image below:

How to run your tests the easy way in Intellij IDEA

After running the test, the results should display green check marks in the Run tab, as seen in the image below:

Result of running the first JUnit test

To finish this example off, add some more Unit Tests to the same ManageTaskDmTests class below onColorSelected:

//…
    @Test
    public void onDoneClickSuccess() {
        ManageTaskEvent event = new ManageTaskEvent(
                ManageTaskEvent.Event.ON_DONE_CLICK,
                ""
        );

        dm.onViewEvent(event);

        assertTrue (view.getNameCalled);
        assertTrue (vm.getTaskCalled);
        assertTrue (view.goToTaskListScreenCalled);
    }

    @Test
    public void onDoneClickException() {
        storage.setWillFail(true);

        ManageTaskEvent event = new ManageTaskEvent(
                ManageTaskEvent.Event.ON_DONE_CLICK,
                ""
        );


        dm.onViewEvent(event);

        assertTrue (view.getNameCalled);
        assertTrue (vm.getTaskCalled);
        assertTrue (view.showMessageCalled);
        assertTrue (view.goToTaskListScreenCalled);
    }
    
    @Test
    public void onIconSelected() {
        ManageTaskEvent event = new ManageTaskEvent(
                ManageTaskEvent.Event.ON_ICON_SELECTED,
                TestData.TASK_ICON
        );

        dm.onViewEvent(event);

        assertTrue (vm.getTaskCalled);
        assertTrue (vm.setTaskCalled);
        assertTrue (view.setIconCalled);
    }
    
    @Test
    public void onStartSuccess() {
        ManageTaskEvent event = new ManageTaskEvent(
                ManageTaskEvent.Event.ON_START,
                0
        );

        dm.onViewEvent(event);

        assertTrue (vm.setTaskCalled);
        assertTrue (vm.getTaskCalled);
        assertTrue (view.setButtonColorCalled);
        assertTrue (view.setNameCalled);
        assertTrue (view.setIconCalled);
    }

    @Test
    public void onStartException() {
        storage.setWillFail(true);

        ManageTaskEvent event = new ManageTaskEvent(
                ManageTaskEvent.Event.ON_START,
                ""
        );

        dm.onViewEvent(event);

        assertTrue (vm.getTaskCalled);
        assertTrue (view.showMessageCalled);
        assertTrue (view.goToTaskListScreenCalled);
    }

//…

Save the file, build, and run the tests under ManageTaskDmTests class again. You should see check marks under the Test Results:

check marks to indicate all the test cases passing on intellij

As you can see, it is important to test exceptions. How you handle them is a project specific detail, and you may wish to create your own exception subclasses for fine grained error handling.

A common pattern I like to use is to have a single entry point into such a class like the decision maker, which is given an object that represents different execution paths. The decision maker uses a method called onViewEvent(...) which is given a ManageTaskEvent object.

I find that this organizes writing the code and tests for such a class quite well. However, not all decision makers will necessarily benefit from having a single entry point.

Controversial Testing Topics

A wise person once said that the duty of a teacher is to point to things and not to fill the student with the teacher’s dogma and opinions. So while I will address a few topics which are frequently argued about regarding testing, I encourage you to try different approaches and see what works for you.

100% Code Coverage

In theory, if you cover every line of executable code with a comprehensive set of well formed tests, then you have logically proved the correctness of your entire project. This is a worthy ideal, but project requirements will dictate whether this goal is useful or silly.

In my experience, the only kinds of projects where I have been able to achieve 100% code coverage, were projects which only needed pure standard library code. For example, I once wrote a compiler front end in pure Kotlin. The entire thing was made via test driven development (TDD) and 100% code coverage was both desired and important.

When it comes to GUI applications, which are tightly coupled to platforms and 3rd party frameworks/libraries, it is a different story altogether. Two common approaches are to either:

  • Use mocks in place of platform, framework, and 3rd party library dependencies.
  • Deploy the application to an actual device or server and use some kind of automated testing framework to drive the interactions.

The first approach is argued by many to be incomplete and unreliable, as you are making the often incorrect assumption that a mock will behave exactly like the real thing. The second approach, while generally more reliable than the first, typically takes a long time to configure, deploy, and run.

It is often recommended to pull any critical logic into an easily unit testable class. The rest will be handled by deploying the application to a test server or device for manual or automated testing.

Test Driven Development

The most common opinions I hear on TDD tend to be irreconcilable. One camp says that TDD is generally a waste of time. As long as you adequately cover critical application logic with tests, it really does not matter whether the tests are written before or after.

The other camp says that you should not write a single line of production code unless you have already written a failing unit test for it. In theory, the time you save catching your errors simply by testing units, and the correctness provided by incrementally proving each piece of the whole, is absolutely worth it.

I treat TDD like every other principle I have come across in programming - Use it only to the extent that it solves more problems that it creates.

If I am writing code where I have no difficult dependencies to deal with, I will most likely apply TDD. I also would do my best when writing code where the correctness of each step is absolutely critical, such as writing a compiler front end.

However, there are certainly situations where I prefer to write the tests afterwards. The most common situation for me is when I am prototyping new approaches, new architectures, or testing new technologies/platforms/languages. After all, you actually need to know what your expected behavior is before you can test against it.

Both camps previously described have valid points, but I never found myself at either extreme. However, I would encourage you not to just read about them, but to try them and gain some practical insight into the matter.

Conclusion

My goal in this article was to provide you with a way to write tests which is repeatable and independent of a large suite of different testing tools. This was done with the intention of giving you a jumping off point to get started writing tests with minimal configuration required.

I hope that I have also carefully pointed out that there are many different ways to write your tests, and I am not necessarily advocating that you follow this approach. There are also many conflicting definitions and approaches in my study of software testing. Another goal of mine was to either clear up or address these conflicts in a balanced way.

My recommendation to you, is to be open to trying different approaches, and to be prepared to spend a few weeks getting comfortable regularly writing tests. I do think it is worth it.

Ryan M. Kay is a self-taught software engineer and content creator. He specializes in software architecture and writing clean, legible, and modular code. Ryan has prepared a comprehensive introductory Java course which is available for free on YouTube.