Skip to main content

A quick and dirty intro to command-line JUnit testing (#1164)

Topics/tags: Miscellaneous, Technical

As a teacher of computer science and computer programming, I try to inculcate a wide variety of habits in my students. One of those is testing, particularly some forms of unit testing. That is, I want my students to write tests (or experiments) that help build confidence that their code achieves what it is supposed to achieve, at least at the small level (e.g., individual procedures or small sets of procedures). I know that writing good tests is hard, so I try to give my students practice. That is, nearly whenever I have students write code, I ask them to write tests or at least think about the tests they would write [1].

This semester, I’m teaching our upper-level course in algorithms. I’m also one of those folks who thinks that you best understand algorithms and data structures whe you implement them. The first assignment asks them to implement data structures they should have already encountered in data structures and algorithms, the third course in the CS major [2]. As you might guess, I asked them to write tests.

Unfortunately, not all of my colleagues feel the same way about testing that I do. And not all of my colleagues teach all the parts of Java that real programers use [3]. So some of my students have asked for guidance on how they write JUnit tests on the command line. This document contains my quick and dirty instructions for using Java and JUnit on the command line, without any additional frameworks or build systems, not even Maven or Ant.

I haven’t attempted anything close to deep guidance on the many features of JUnit or the common aspects of unit testing frameworks. I haven’t attempted to describe the unit testing habits I expect, from careful thought about edge cases to looking for ways to automate more complicated and even randomized tests, such as the test for binary search that I adapted from Jon Bentley’s book. Those are topics for another day.

Here goes.


Getting started

JUnit is a standard unit testing framework for Java. You use JUnit slightly differently on each development platform for Java. The JUnit User Guide provides instruction for most major platforms. It’s a little bit vague on command-line Java, and that’s what we will often use, so I will focus primarily on those issues.

What should you know?

A test is just a void method that is annotated as a test using @Test. A typical test method contains assertions about the expected behavior or values of a procedure. For example, in testing a square procedure, I might assert that square(4) is 16. We tend to write different test procedures when testing different aspects of our procedure or program. (At least I tend to do that; different testers have different customs.)

What can you assert? The class org.junit.jupiter.api gives the standard assertion methods. The most common ones are

  • assertEquals, which checks that two expressions have the same value (or close to the same value, if you use the variant designed for real numbers);
  • assertTrue, which checks that a Boolean expression returns true;
  • assertAll, which checks whether a sequence of executable expressions all succeed (do not throw exceptions); and
  • assertThrows, which checks whether an executable expression throws a given type.

Both assertAll and assertThrows require what Schemers often call thunks and what JUnit calls executables. An executable is an object that implements the Executable interface, providing a method called execute. Since that interface requires only one method, we can create executable objects on the fly using lambda expressions. For example, if a is an [ArrayList](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ArrayList.html), then an executable that adds a null at index 5 might read () -> a.set(5, null).

Suppose we want to test whether adding that null throws an exception (which it should, if the ArrayList has fewer than four elements). Here’s what we might write.

  @Test
  void testInvalidAddition() {
    ArrayList<String> a = new ArrayList<String>();
    assertThrows(IndexOutOfBoundsException,
                 () -> a.set(5, null));
  } // testInvalidAddition()

Grouping tests

Of course, tests can’t live by themselves. Like most methods in Java, they need to appear in a class.

class MyArrayListTests {
  @Test
  void testInvalidAddition() {
    ArrayList<String> a = new ArrayList<String>();
    assertThrows(IndexOutOfBoundsException.class,
                 () -> a.set(5, null));
  } // testInvalidAddition()
} // class MyArrayListTests

And, as is typical in Java classes, we need to import things. When working with JUnit, we typically import the assertion methods (which are static) and the @Test annotation.

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class myArrayListTests {
  // ...
} // class MyArrayListTests

Preparing to run tests

We’ll be running the standalone JUnit console. You’ll need to download the appropriate jar file from https://mvnrepository.com/artifact/org.junit.platform/junit-platform-console-standalone and store it somewhere sensible. I keep mine in ~/lib, but you can choose whatever you’d like. I’ve been using version 1.7.2, which is relatively recent.

Running tests

Once we’ve set up the tests in a class (in a .java file), we need to compile the .java file and then run it. (Note that we should compile all the .java files we’re working with.)

$ ls
MyArrayListTests.java
$ javac MyArrayListTests
... lots of errors, such as package org.junit.jupiter.api does not exist

Whoops. We need to make sure that we have the .jar file on our CLASSPATH.

$ export CLASSPATH=/path/to/junit-platform-console-standalone-1.7.2.jar:$CLASSPATH
$ ls
MyArrayListTests.java
$ javac MyArrayListTests
$ ls
MyArrayListTests.class  MyArrayListTests.java

The .class file is the compiled version.

Now we’re ready to run the tests. There’s probably a better approach than the one I use, but this seems to suffice. The -cp . flag says to look for class files in the current directory and the -c MyArrayListTests gives the name of the class.

$ java -jar /path/to/junit-platform-console-standalone-1.7.2.jar -cp . -c MyArrayListTests

Thanks for using JUnit! Support its development at https://junit.org/sponsoring

╷
├─ JUnit Jupiter ✔
│  └─ MyArrayListTests ✔
│     └─ testInvalidAddition() ✔
└─ JUnit Vintage ✔

Test run finished after 76 ms
[         3 containers found      ]
[         0 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         1 tests found           ]
[         0 tests skipped         ]
[         1 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         0 tests failed          ]

That’s about it. You should be able to figure out the rest. Send me questions if you can’t (and you’re one of my students).


Postscript: Here’s the complete file.

import java.util.ArrayList;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class MyArrayListTests {
  @Test
  void testInvalidAddition() {
    ArrayList<String> a = new ArrayList<String>();
    assertThrows(IndexOutOfBoundsException.class,
                 () -> a.set(5, null));
  } // testInvalidAddition()
} // class MyArrayListTests

[1] Strangely enough, the course in which I was worst about enforcing testing was probably CSC-324, Software Design.

[2] Officially, the course has a title something like Object-Oriented Design, Data Structures, and Algorithms. Historically, that course is called CS2 in the CS education literature. At Grinnell, we’ve inserted an extra course in the introductory sequence that does some linked data structures while getting students used to memory management before we hit them hard with ADTs, data structures, and algorithms.

[3] Admittedly, I don’t, either.


Version 1.0 released 2021-08-30.

Version 1.0.1 of 2021-08-30.