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.
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 {
@Test
public void testAddAndRemove() {
IList<Integer> list = new DoubleLinkedList<>();
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 = new DoubleLinkedList<>();
int amount = 5134;
for (int i = 0; i < amount; i++) {
list.add(i);
}
assertTrue(list.size() == amount);
}
}
A few things to note:
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.)
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 some
"assert" method imported from org.junit.Assert
.
The @Test
annotation can take an optional "timeout"
parameter. For example, see line 25. 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 strongly recommend you add a timeout to all unit tests you write.
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.
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 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:
assertTrue(boolean condition)
assertTrue(String message, boolean condition)
Asserts that the condition is true (and fails the test if it isn't). You can optionally provide a custom error message that's printed on failure as the first argument.
assertEquals(T expected, T actual)
assertEquals(String message, T expected, T actual)
Asserts that the two objects are equal to one another, using their
.equals(...)
method. If the two objects aren't equal,
JUnit will display a nice error message containing the expected
and actual result. You can also override this error message with your
own.
assertArrayEquals(T[] expected, T[] actual)
assertArrayEquals(String message, T[] expected, T[] actual)
Same as above, except for arrays.
fail()
fail(String message)
Causes the test to fail, either with no error message or with a custom one.
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 testRemoveCorrectlyThrowsEmptyContainerException() {
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. It's good practice to include a comment mentioning
// this, though.
}
// If the method somehow throws a *different* unexpected exception,
// the "catch" block won't catch it and the test will fail.
// For example, if list.remove() throws a NullPointerException, something
// has probably gone horribly wrong and we *want* our test to fail.
}
If you have never seen a try-catch block before, here's more information on what they are.
As a note of warning: as a matter of style, the only place you should be using try-catch blocks is within your tests. There are times when using try/catch blocks inside your main code is the correct thing to do, but you will never encounter those scenarios within this class.