CSE 373, Summer 2019: JUnit Guide

About JUnit

JUnit is a popular library used to help you unit test your code. Although it comes packed with all kinds of features to help you write sophisticated tests, we'll be using only a few of these features.

This guide covers JUnit only on a basic level. If you want to use some of the more advanced JUnit features, feel free to do so. Note: if you plan on doing this, keep in mind that we are using JUnit 5 in this class.

The anatomy of a JUnit test suite

Here's a basic example of what a JUnit test suite looks like:

package mypackage;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import org.junit.jupiter.api.Test;

import datastructures.concrete.DoubleLinkedList;
import datastructures.interfaces.IList;

import misc.BaseTest;

public class TestDoubleLinkedListSimple extends BaseTest {
    @Test
    void testAddAndRemove() {
        IList<Integer> list = new DoubleLinkedList<>();

        list.add(3);
        list.add(7);
        list.add(9);

        assertThat(list.remove(), is(9));
        assertThat(list.remove(), is(7));
        assertThat(list.remove(), is(3));
    }

    @Test
    void testSize() {
        IList<Integer> list = new DoubleLinkedList<>();

        int amount = 5134;

        for (int i = 0; i < amount; i++) {
            list.add(i);
        }

        assertThat(list.size(), is(amount));
    }
}

A few things to note:

  1. Line 3 is importing the assertThat(...) static method. This method will do nothing if the asserted statement is true (e.g. list.remove() is actually 9 the first time it is called on line 21), but will cause the current test to fail if the assertion is false.

    (For more details on what exactly a static method is, see below.)

  2. A JUnit test suite is a regular class consisting of one or more methods marked with the @Test annotation. Each one of these methods counts as a single unit test. So, this example class contains two unit tests.

    Unit test names are typically prefixed with the word "test" followed by a description of what they're testing. This is only a convention: you can name the methods whatever you want, as long as you remember to add the @Test annotation.

    (For more details on what exactly an annotation is, see below.)

  3. A unit test method will almost always contain one or more calls to the assertThat method imported from org.hamcrest.MatcherAssert.

  4. Our test suites will always ultimately extend the BaseTest class, which contains a few useful utilities and workarounds for JUnit quirks.

    Normally, when using JUnit, there's no need to extend any class.

  5. Although our example does not demonstrate this, your may add and call as many private helper functions you wish. Just remember, do NOT add the @Test annotation to your helper methods! After all, your helper methods are not themselves tests.

  6. JUnit will call your test methods in basically random order. Make sure you don't assume they run in any particular order or try to share any data between them!

What are static imports?

A static import is a way of directly importing a static method, as opposed to a class.

Static imports are useful mainly because they make typing code a little easier. For example, suppose you were writing a program involving a lot of math. Normally, you'd need to scatter your code with calls to things like Math.sin(...) and Math.cos(...).

Instead, what you could do is add the following lines:

import static java.lang.Math.sin;
import static.java.lang.Math.cos;

Now, you no longer need to add Math. in front of everything and can just do sin(...) and cos(...), which is pretty convenient.

What are annotations?

Annotations are a form of metadata that you can attach to declarations, types, and certain expressions.

Annotations will have no direct effect on what the program does when run. Rather, they're used to do one of the following:

  1. Provide information to the Java compiler
  2. Provide compile-time information to certain tools and libraries
  3. Attach extra information that can (sometimes) be inspected during runtime when doing meta-programming related things.

The @Test annotation has no special meaning to the Java compiler -- it's used primarily by the JUnit library.

Another annotation you might have encountered already is the @Override annotation. When you mark a method with this annotation, you're telling Java that method is supposed to have overridden some other method in a super class. If the annotated method isn't overriding anything (perhaps you made a typo?) the Java compiler will refuse to compile your code.

Basically, once everything is compiled, the @Override annotation has zero impact on your code. It exists mainly to help the compiler catch mistakes you might have made.

More helpful methods

You can use the assertThat and is() method for almost anything because they're extremely flexible, but org.hamcrest provides a few other methods that you may find handy. Here's a list of some of the methods available that you can use instead of in combination with is; here's a summary of the more relevant ones:

  • not(...)

    Negates the thing it wraps around. For example, assertThat(2 + 2, is(not(5)) passes. So you can use not to check that things are not true / not certain values.

  • contains(...) and containsInAnyOrder(...)

    Useful to verify that all the values expected to be in the data structure are actually contained. These are implemented by looping over the data structure with the data structure's iterator. So, this requires that an Iterator (and the Iterable interface) are implemented. Luckily this is true for most of the data structures we will implement in this course (not ArrayHeap and ArrayDisjointSet).

  • hasItem(...)

    Makes sure that a single value is contained inside your data structure, by making sure that it is eventually returned by your iterator. So, this also requires that an Iterator and the Iterable interface are implemented.

  • assertThrows

    (Note: This one is from org.junit) Makes sure that a specific exception is thrown when executing the given code. Fails if the specified exception is not thrown. (see below for an example)

Testing exceptions

Testing that you're throwing the correct exception will involve learning some new syntax

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

@Test
void testRemoveCorrectlyThrowsEmptyContainerException() {
    IList<Integer> list = this.makeList();

    assertThrows(EmptyContainerException.class, () -> { list.remove(); });
}

The EmptyContainerException.class syntax is accessing information about the EmptyContainerException class, and by passing it in as the first parameter of assertThrows, we're specifying this is the exception we expect.

The second parameter, () -> { list.remove(); } specifes an example of a lambda. We can use lambdas in our code to flexibly use functions as parameters, values, and basically use code snippets on the fly. Here when a lambda expression is passed to assertThrows as the second parameter, we're specifying that when this code runs it should throw the exception specified by the first parameter.

So in summary, line 7 above is saying: when we call list.remove(), make sure an EmptyContainerException is thrown. If not, fail the test.

Note: you may put multiple lines inside the curly braces. If you only have one line of code to run, you can even omit the curly braces.

The syntax might be new and potentially daunting, but it turns out lambdas are very useful and are good to know about. Of course, feel free to ask the course staff for help with any syntax problems you run into.