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.
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:
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.)
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.)
A unit test method will almost always contain one or more calls to the assertThat method imported from org.hamcrest.MatcherAssert
.
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.
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.
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!
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 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.