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 4 in this class. JUnit 5 was released very recently, so make sure you're looking at the correct version of the documentation.

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.junit.Assert.assertTrue;
import org.junit.Test;

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

import misc.Cse373BaseTest;

public class TestDoubleLinkedListSimple extends Cse373BaseTest {
    private <T> IList<T> makeList() {
        return new DoubleLinkedList<>();
    }

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

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

        assertTrue(list.remove() == 9);
        assertTrue(list.remove() == 7);
        assertTrue(list.remove() == 3);
    }

    @Test(timeout=1000)
    public void testSize() {
        IList<Integer> list = this.makeList();

        int amount = 5134;

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

        assertTrue(list.size() == amount);
    }
}

A few things to note:

  1. Line 3 is importing the assertTrue(...) static method. This method will do nothing if the argument you pass in is true, but will cause the current test to fail if the argument 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 some "assert" method imported from org.junit.Assert.

  4. The @Test annotation can take an optional "timeout" parameter. For example, see line 23. The timeout is set to 1000 milliseconds, or one second. So, if the testSize method isn't done after 1 second, that test will fail.

    We recommend you add a timeout to all unit tests you write.

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

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

  6. Your unit test methods can call any number of helper functions.

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

// Or, if you want everything:
import static.java.Math.*;

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 "assert" methods

You can use the assertTrue method for almost anything, but JUnit provides a few other "assert" methods that you may find handy. Here's an exact list of all of the methods available; here's a summary of the more relevant ones:

Testing exceptions

Testing that you're throwing the correct exception is unfortunately a little awkward. Here's an example of how to do it:

@Test(timeout=1000)
public void testRemoveThrowsRightException() {
    IList<Integer> list = this.makeList();

    try {
        // Try running the operation we expect should throw an exception
        // inside of a "try" block
        list.remove();

        // If it somehow succeeds, that's bad, and we want to fail.
        fail("Expected an EmptyContainerException");

    } catch (EmptyContainerException ex) {
        // We're going to try catching the specific exception we're
        // looking for. If we catch it, we do nothing: everything
        // is ok.
    }

    // If the method somehow throws a *different*, unexpected, exception,
    // the "catch" block won't catch it and the test will fail.
}

If you have never seen a try-catch block before, here's more information on what they are.

Writing good tests

Here are some things you might want to try checking when testing:

  1. The happy cases:

    Test how your clients will normally use your code.

  2. Errors and exceptions:

    Do you throw the correct exceptions when bad input is given? Did you forget to implement a certain exception? Do you check for an error condition too late?

  3. Preconditions and postconditions:

    Try checking/stress-testing your preconditions. Are your postconditions always satisfied afterwards?

  4. Empty and null cases:

    What happens when you pass in zero? An empty string or list? A null object?

  5. Internal invariants:

    All data structures have "invariants" — assumptions (usually about their fields) that must be true both before and after any method is called. Are those assumptions actually true? Or does a method break them by accident?

  6. Edge cases:

    Test for edge cases and off-by-one errors. Look for "boundaries" in your input data. Try generating randomized data to hunt for weird edge cases.