DEV Community

loading...

Step by Step Introduction to Unit Testing in Java

gervg profile image Gervin Guevarra ・13 min read

Table of Contents


Introduction

Hey folks! So I kinda promised to someone I forgot who that I'll write a unit testing guide for this group. There must be hundreds of similar guides out there in the wild, but I feel like most of those I've seen are too short and doesn't really explain the rationale behind unit testing. I think unit testing is one of the most underrated skill that a programmer should learn and sadly it isn't taught in schools (in our country, at least). So here's my take on a (hopefully) gentle introduction to the world of automated testing.

Anyway, enough with my babbling. Let's get right into it.

Naive way of testing code

When we first started learning programming, what we'd normally do is write code, run the program. Then conduct tests by entering some inputs hoping that the output is as we desired. If not, we go back writing our code again and fixing bugs.

Well, there's nothing wrong with that. In fact, for your entire life as a programmer you'd be doing all these three things:

1) write code

2) run the program

3) test the behavior of your program

Then repeat again

But you won't always be writing simple console applications. Sooner or later, you'll find yourself doing repetitive tests with slightly varying inputs.

In the real world where you'd be dealing with much much bigger and more complex software, doing those three things every time will be very time consuming. There will also be cases where you need to isolate your changes and just test a specific part rather than the whole system.

What is Unit testing?

So instead of running and testing your entire program every time you have to a change in your code, we can automate it by writing code to test our code. More specifically, write code that would test a single part of the system isolated from everything else. That's unit testing.

An analogy

For example, you are building a house and you need some light bulbs. So you go to the store and buy a light bulb. In order to make sure that the light bulb you just bought works, you need to test it.

But the good thing about it is that it's independent of your house. You can test it by itself. No need to actually install it in your house and see if it lights up. Instead, you or the sales assistant can install it to a test bulb socket (found in most hardware or appliance stores) and see if it lights up. You are essentially testing a single unit of a light bulb which is meant to be part of your house (i.e. the entire system) under construction.

Imagine if it wasn't possible to test the light bulb alone. You have to go back to your house first, finish building it, install the light bulbs and test if it actually switches on. Quite a hassle, isn't it?

Getting your feet wet

Prerequisites

Before taking this guide, be sure that you are already familiar with Java language as I will not be going through the detail of compiling and running your Java program. Also be sure to have an internet connection as we will be downloading some tools, and libraries. This guide also assumes that you have an existing project where you want to add unit testing. If not, you can just copy-paste the Java files below and follow along.

It's also worth noting that the guide, for now, is also written to suit MacOS or Linux users. But experienced Windows users may also want to try anyway (and let me know where it gets difficult! I'll make updates on this guide where necessary).

Below is short list of everything you need:

  • Java 8 or higher (I'm using Java 8. But to follow this guide, it doesn't matter if you're using a newer version)
  • Favorite Code editor or IDE (I use IntelliJ IDEA myself, but you're free to use whatever you want)
  • Gradle build tool (download and follow the guide here)
  • Command Line (Shell or Command Prompt)
    • we'll be doing things from scratch, so having familiarity working in terminal is a great plus
    • this also makes this guide IDE-agnostic, meaning you don't have to be very dependent on a specific IDE just to make this work
    • if you're on Windows, you may opt to use WSL or the bash prompt that comes with Git (if you are already using one).

A sample existing code

Say we have an existing console application which accepts input of any length from the user and then our program outputs the reverse of the input. Classic problem.

So we can implement this by writing the following:

You might notice a "bug" here. That's intentional. We'll delve into that later

File structure 1

At this point, your project structure should look similar to this. It's alright if you don't have the .idea folder or the *.iml file. Those are just IntelliJ IDEA generated files.

Setting up the testing framework

The testing framework that we're gonna use is JUnit. To add it in our project, we need a dependency manager such as Gradle.

💡 Dependency Manager
The great thing about OOP is that it allows us to reuse somebody else's code. Those reusable pieces of code often mature enough that they can be standalone libraries or frameworks. They then get redistributed by various means, either by downloading the JAR files, as in the case of Java, or getting the source code and building it by yourself. These libraries or frameworks become the dependencies of your project. Some of these libraries/frameworks have dependencies of their own, so you also have to take care of them and add them to your project.

But doing so multiple times over the duration of your project can be time-consuming, entails difficulties, and is a very repetitive processes. This is where dependency manager comes in.

A dependency manager is a tool that helps you download libraries and/or frameworks, as well as their dependencies, to add to your project; while also keeping track of the version you are using for each. Some examples of popular dependency managers for Java projects are Gradle and Maven (which also functions as build tools).

For further reading about dependency managers and what they can do, check out this article by Seun Matt in Medium

If you have prepared the prerequisites listed earlier, you should have Gradle already installed in your system. To check, enter the following in your terminal and it should output a directory where you installed Gradle.

$ which gradle
Enter fullscreen mode Exit fullscreen mode

Next, we need to turn our existing console application project into a Gradle project. Expand each step by clicking the drop-down then follow the instructions

  • Go to your project directory

    In my case, my project is saved in Users/gerv/Source/HelloUnitTesting. I can use ~ as short hand for my User home directory.

    $ cd ~/Source/HelloUnitTesting
    
  • Initialize a Gradle project

    Simple enter the following command and follow the on-screen instructions to setup gradle for your project

    $ gradle init
    

    If you're confused, you can just follow the screenshot below.
    Gradle init output

    You might notice that my shell prompt is different. That's because I'm using zsh with oh-my-zsh, but that's a topic for another day. For now, think of that fancy arrow as the $ sign you normally see.

  • Run your first Gradle build

    $ gradle build
    

    Then you should see an output similar to this:
    Gradle build output

You might notice that there are a bunch of files added to your project folder. These are files that are generated by gradle init and will be used by Gradle when building your project. You may happily ignore them for now, but I want to quickly introduce you to one of them, the build.gradle file. This is the file that contains the list of your dependencies and repositories (from which your dependencies will be downloaded). Take a quick look at it and notice that JUnit is already added; this because we picked JUnit earlier when we ran gradle init .

You may also notice that main and test folders are added to the project. We will use them to reorganize our code.

Separating source code and test code

Gradle isn't smart enough to know that you already have existing code in your project, and assumes that you are starting a Gradle project from scratch. So it creates its own folders, package, the class named App.java with the main() method.

After running gradle init , your project directory should look like this.
File structure after gradle init

But we already have ConsoleApplication.java and that serves as the main entry point of our application so we can just get rid of App.java. We also don't need the package HelloUnitTesting since we already have an existing one from before. Delete them as you normally would, or if you're like me and you like doing everything in terminal

$ rm -r src/main/java/HelloUnitTesting
Enter fullscreen mode Exit fullscreen mode
$ rm -r src/test/java/HelloUnitTesting
Enter fullscreen mode Exit fullscreen mode
Project directory after deleting some files

Then we need to reorganize our code and put them inside the main/java/ folder. To do this, you can either drag the com.gerv.guev (or whatever your existing package name is) to main/java/ or do it via terminal again

$ mv src/com src/main/java/com
Enter fullscreen mode Exit fullscreen mode

If you're also using IntelliJ IDEA, you might notice that main/java is marked like a package even though it isn't. To make them appear like normal folders, simply right click on the src then point to Mark Directory As and pick Unmark as Sources Root. Then right click on java folder under main then Mark Directory As and pick Sources Root. Do the same thing with the java folder under test, but pick Test Sources Root. For the resources folder, mark them as Resources Root and Test Resources Root for main and test respectively.

If you've carefully followed the instructions, your project directory should now look something like this:

File structure after setting up Gradle

To quickly check if everything still works, run this and it should tell you "Build Successful". You may also want to try and run your program if it still works as before.

$ gradle build
Enter fullscreen mode Exit fullscreen mode

If your gradle build is successful but your IDE is complaining (i.e. squiggly red lines, you can just reimport/reopen your project and hopefully your IDE recognizes that it's now a Gradle project.

Quick Recap!
That's a lot of things we've already covered. Here's what we've done so far:

  • turn our project into a Gradle project
  • add JUnit to our dependencies
  • Reorganize our project folders

Writing your first unit test

Now that we have finished setting up our project and its dependencies, we are now ready to write our unit test!

Under the test/java folder, create a package com.gerv.guev or whatever package name you already used in your project. Then create a file named MyStringUtilitiesTest.java. You may copy the contents of the class for now, I'll explain what it does along the way.

Then we are going to add a test method that actually does nothing. We're taking very small steps to make sure we're not making mistakes and everything is still working as it is.

In the code above, @Test is an annotation that tells our compiler that the method that we just wrote is a test method. @Test annotation is located under the org.junit package (notice the import statement above).

A method marked as a test method will be checked by the test runner if it satisfies some conditions we are expecting. In this case, the test should always pass because we told it to assert true which will always be true no matter what. The point of writing this the first time is to check if our unit testing framework was really set up correctly. After you've written one or if you're already familiar with the testing framework your are using, there's no need to write this one every time.

To run the test, type in this command

$ gradle test --tests="com.gerv.guev.MyStringUtilitiesTest.someUselessTest"
Enter fullscreen mode Exit fullscreen mode

In the command above, we are telling Gradle to run the test found in a fully qualified name. In this case we're telling it to specifically run someUselessTest() test method.

A fully qualified name consists of the package name, class name, and method name. It represents the hierarchical location of your file or folder and should always be unique.

But if you have multiple tests inside a class or package, you can just replace it with an asterisk (*) and it should still run

$ gradle test --tests="com.gerv.guev.MyStringUtilitiesTest.*"
Enter fullscreen mode Exit fullscreen mode

Running the test and using the results to debug

Now, let's replace that useless test with a real one to test our earlier code for reversing a string.

Go ahead and run the test using the command mentioned earlier, and you should see an error similar to this

> Task :test FAILED

com.gerv.guev.MyStringUtilitiesTest > shouldReturn_ReverseString FAILED
    org.junit.ComparisonFailure at MyStringUtilitiesTest.java:14

1 test completed, 1 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/gerv/Source/HelloUnitTesting/build/reports/tests/test/index.html

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s
3 actionable tasks: 1 executed, 2 up-to-date
Enter fullscreen mode Exit fullscreen mode

This tells us that our test failed. Gradle has a pretty neat way of telling this to us and generates an HTML report. Copy that file path and open it using your browser.

Alt Text

From here it says: org.junit.ComparisonFailure: expected:<[olleh]> but was:<[????o]> on the first line of the stack trace. It tells us many things:

  1. our input string was not reversed hence it is not equal to our expected output "olleh"
  2. the actual result does not contain the correct characters or is probably empty. Instead, it's displaying multiple question marks.
  3. The actual output has the same length as our expected output. It has 5 characters.
  4. The last character "o" is still the last character in the actual output

Don't fear the stack trace!
Most beginners find the error messages in the stack trace intimidating, but it's actually quite easy to decipher. All you have to do is to read the first line to have a general idea of what caused the failure. If you still don't know at first glance what the error was, skim through the lines and look for your package and class ****then check the line number.

In our example, it says:
org.junit.ComparisonFailure: expected:<[olleh]> but was:<[????o]>

then I skip lines until I see a familiar package name:
at com.gerv.guev.MyStringUtilitiesTest.shouldReturn_ReverseString(MyStringUtilitiesTest.java:14)

That tells us that there was a ComparisonFailure at line 14 of our MyStringUtilitiesTest class.

Given those findings, we can check back our code to see what was wrong. Here's the current code now with comments. Can you spot what's wrong?

From our original code, what we did is we took each letter of the input from left to right then transferring it to the new character array from right to left but we did not move the index. To fix this, we have to decrement the index for it to move from right to left.

Now run your test again and it should give that sweet success

gradle test --tests="com.gerv.guev.MyStringUtilitiesTest.*"

BUILD SUCCESSFUL in 1s
3 actionable tasks: 2 executed, 1 up-to-date
Enter fullscreen mode Exit fullscreen mode

Suppose we are going to add features in our string utilities class, like detection of palindrome

Then all we have to do is to write another unit test and run all of them. The beauty of unit tests is that you'll always have a proof that your older features are still working even after adding new ones. And there's no need to run your console application every time just to do a manual test.

You may continue adding other tests, say a different input word or maybe an entirely different test, then run it as usual. I am leaving that as an exercise for you.

Final notes

We've barely scratched the surface of unit testing and there's a lot more to it than just testing inputs and outputs. We haven't even discussed yet its best practices but that's enough for now. I hope that you get the rough idea of how to use unit testing to your advantage.

Further Reading

  • Parasoft has a quick guide on setting up JUnit and it even teaches how to use it without build tools like Gradle or Maven. Go check it out here
  • There are other excellent testing frameworks available for Java such as TestNG and Spock. I personally prefer Spock over JUnit because of some features and syntactic sugars. The caveat is it's written using Groovy which is a dynamically-typed language. It might be hard for beginners to quickly grasp it on top of understanding unit-testing.
  • For .NET users, there's XUnit as the de facto testing framework for .NET applications. It's (arguably) the successor of the older NUnit. For other languages, just try appending the first few letters of what ever language you're using then "-Unit". For example, JSUnit, PhpUnit, PyUnit, etc.
  • Some software development techniques such as Test-Driven Development (TDD) and Behavior-Driven Development (BDD) are anchored in the mastery of unit testing. These techniques will help you consciously develop features while also maintaining robustness of your system over time.
  • If you're an intermediate or advanced programmer, I strongly recommend that you read Martin Fowler's articles on Unit Testing, other levels of testing and the concept of self-testing code
  • Unit testing can also help you abstract away the layers of your application by using Test Doubles. You can read another Fowler's article here or the equally comprehensive blog of Mark Seeman at Microsoft.

Discussion (0)

pic
Editor guide