Unit testing frameworks streamline the process of writing test drivers with automated oracles.
JUnit is a unit testing framework for Java.
Introduced in 2002 by Kent Beck and Erich Gamma, JUnit (and it’s xUnit relations) have become near-universal.
Reputedly, first version was prototyped during a Trans-Atlantic flight as a way for Beck to teach Java to Gamma.
Martin Fowler: “Never in the field of software development was so much owed by so many to so few lines of code.”
A 2015 survey shows how pervasive JUnit is become.
Junit…
Unit testing frameworks speed the process of
We will be looking at JUnit 5, also known as JUnit Jupiter.
We’ll start with a simple example, a mailing list application. The MailingList
is a collection of Contact
s.
package mailinglist;
/**
* A contact is a name and address.
* <p>
* For the purpose of this example, I have simplified matters
* a bit by making both of these components simple strings.
* In practice, we would expect Address, at least, to be a
* more structured type.
*
* @author zeil
*
*/
public class Contact
implements Cloneable, Comparable<Contact> {
private String theName;
private String theAddress;
/**
* Create a contact with empty name and address.
*
*/
public Contact ()
{
theName = "";
theAddress = "";
}
/**
* Create a contact
* @param nm name
* @param addr address
*/
public Contact (String nm, String addr)
{
theName = nm;
theAddress = addr;
}
/**
* Get the name of the contact
* @return the name
*/
public String getName()
{
return theName;
}
/**
* Change the name of the contact
* @param nm new name
*/
public void setName (String nm)
{
theName= nm;
}
/**
* Get the address of the contact
* @return the address
*/
public String getAddress()
{
return theAddress;
}
/**
* Change the address of the contact
* @param addr new address
*/
public void setAddress (String addr)
{
theAddress = addr;
}
/**
* True if the names and addresses are equal
*/
public boolean equals (Object right)
{
Contact r = (Contact)right;
return theName.equals(r.theName)
&& theAddress.equals(r.theAddress);
}
public int hashCode ()
{
return theName.hashCode() + 3 * theAddress.hashCode();
}
public String toString()
{
return theName + ": " + theAddress;
}
public Object clone()
{
return new Contact(theName, theAddress);
}
/**
* Compare this contact to another.
* Return value > 0 if this contact precedes the other,
* == 0 if the two are equal, and < 0 if this contact
* follows the other.
*/
public int compareTo (Contact c)
{
int nmcomp = theName.compareTo(c.theName);
if (nmcomp != 0)
return nmcomp;
else
return theAddress.compareTo(c.theAddress);
}
}
package mailinglist;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
/**
* A collection of names and addresses
*/
public class MailingList implements Cloneable {
private LinkedList<Contact> theContacts;
/**
* Create an empty mailing list
*
*/
public MailingList() {
theContacts = new LinkedList<Contact>();
}
/**
* Add a new contact to the list. Contacts should be kept in ascending
* order with no duplicates.
*
* @param contact new contact to add
*/
public void addContact(Contact contact) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.compareTo(contact);
if (compared > 0) {
it.previous();
it.add(contact);
return;
} else if (compared == 0) {
return;
}
}
theContacts.add(contact);
}
/**
* Remove one matching contact
* @param contact remove a contact equal to contact
*/
public void removeContact(Contact contact) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.compareTo(contact);
if (compared > 0) {
return;
} else if (compared == 0) {
it.remove();
return;
}
}
}
/**
* Remove a contact with the indicated name
* @param name name of contact to remove
*/
public void removeContact(String name) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.getName().compareTo(name);
if (compared > 0) {
return;
} else if (compared == 0) {
it.remove();
return;
}
}
}
/**
* Search for contacts
* @param name name to search for
* @return true if a contact with an equal name exists
*/
public boolean contains(String name) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.getName().compareTo(name);
if (compared > 0) {
return false;
} else if (compared == 0) {
return true;
}
}
return false;
}
/**
* Search for contacts
* @param name name to search for
* @return contact with that name if found, null if not found
*/
public Contact getContact(String name) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.getName().compareTo(name);
if (compared > 0) {
return null;
} else if (compared == 0) {
return c;
}
}
return null;
}
/**
* combine two mailing lists
*
*/
public void merge(MailingList anotherList) {
// For a quick merge, we will loop around, checking the
// first item in each list, and always copying the smaller
// of the two items into result
LinkedList<Contact> result = new LinkedList<Contact>();
ListIterator<Contact> thisPos = theContacts.listIterator();
ListIterator<Contact> otherPos = anotherList.theContacts.listIterator();
while (thisPos.hasNext() && otherPos.hasNext()) {
Contact thisContact = thisPos.next();
Contact otherContact = otherPos.next();
int comp = thisContact.compareTo(otherContact);
if (comp == 0) {
result.add(thisContact);
} else if (comp < 0) {
result.add(thisContact);
otherPos.previous();
} else {
result.add(otherContact);
thisPos.previous();
}
}
// Now, one of the two lists has been entirely copied.
// The other might still have stuff to copy. So we just copy
// any remaining items from the two lists. Note that one of these
// two loops will execute zero times.
while (thisPos.hasNext()) {
result.add(thisPos.next());
}
while (otherPos.hasNext()) {
result.add(otherPos.next());
}
// Now result contains the merged list. Transfer that into this list.
theContacts = result;
}
/**
* How many contacts in list?
*/
public int size() {
return theContacts.size();
}
/**
* Return true if mailing lists contain equal contacts
*/
public boolean equals(Object anotherList) {
if (!(anotherList instanceof MailingList)) {
return false;
}
MailingList right = (MailingList) anotherList;
if (size() != right.size()) { // (easy test first!)
return false;
}
ListIterator<Contact> thisPos = theContacts.listIterator();
ListIterator<Contact> otherPos = right.theContacts.listIterator();
while (thisPos.hasNext() && otherPos.hasNext()) {
Contact thisContact = thisPos.next();
Contact otherContact = otherPos.next();
if (!thisContact.equals(otherContact)) {
return false;
}
}
return true;
}
public int hashCode() {
return theContacts.hashCode();
}
public String toString() {
StringBuffer buf = new StringBuffer("{");
boolean first = true;
for (Contact c: theContacts) {
if (!first) {
buf.append(", ");
}
first = false;
buf.append(c.toString());
}
buf.append("}");
return buf.toString();
}
/**
* Deep copy of contacts
*/
public Object clone() {
MailingList result = new MailingList();
for (Contact c: theContacts) {
result.theContacts.add((Contact)c.clone());
}
return result;
}
}
Set up an Eclipse project with these two files
In the Eclipse Build Path, add a library: JUnit,
New...Junit Test Case
JUnit Jupiter
is selected. Fill in the name (TestMailingList
) and the “Class Under Test” (mailinglist.MailingList
). Click Next
.MailingList
: the constructor, addContact
, removeContact
, merge
, and clone
. Click Finish
.
We wind up with something like this:
package mailinglist;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class TestMailingList ➀ {
@Test ➁
void testMailingList() {
fail("Not yet implemented");
}
@Test
void testAddContact() {
fail("Not yet implemented");
}
@Test
void testRemoveContactContact() {
fail("Not yet implemented");
}
@Test
void testRemoveContactString() {
fail("Not yet implemented");
}
@Test
void testMerge() {
fail("Not yet implemented");
}
@Test
void testClone() {
fail("Not yet implemented");
}
}
➀ A JUnit test is a class.
➁ It contains one or more “test cases” or “test functions” – member functions that return void
, take no parameters, and are marked with @Test
.
Each test case needs to
But, of course, right now none of that has been written yet.
A test case can
fail(...)
function is called explicitly).Let’s think about testing the MailingList
constructor:
MailingList() {
⋮
}
What do we think should be true when this has completed?
MailingList
.Contacts
yet.We go to our skeleton testMailingList()
and try to express those ideas:
@Test
void testMailingList() {
MailingList ml = new MailingList();
assertEquals (0, ml.size());
}
Is that enough?
MailingList
functions that we did not select to create separate tests for.MailingList
without changing it: contains
, getContact
, size
, equals
, hashCode
, and toString
.
MailingList
.After construction…
contains
should return false for all names.getContact
should return null for all names.size
should be zero.equals
should return true when compared to another freshly-constructed list.hashCode
s should be equal whenever equals
is true.toString
, as the documentation does not require a specific output format. (The primary role of toString
in most classes is to provide debugging output.) But it should return some kind of string. @Test
void testMailingList() {
MailingList ml = new MailingList(); ➀
assertEquals (0, ml.size()); ➁
assertFalse (ml.contains("Jones"));
assertNull (ml.getContact("Smith"));
MailingList ml2 = new MailingList();
assertEquals (ml2, ml);
assertEquals (ml2.hashCode(), ml.hashCode());
assertNotNull(ml.toString());
}
Every good JUnit test can be read as
Reading the unit tests is often one of the easiest ways to discover how to use an unfamiliar class.
Now would be a good time to run the test.
In Eclipse, right-click on the TestMailinglist.java
file and select Run as...JUnit test
.
We’ve now seen several examples of JUnit assertions.
These are fundamental, but not as descriptive as other options:
assertTrue(
condition )
: condition should be true.assertFalse(
condition )
: condition should be false.More common basic assertions:
assertEquals(
x , y)
: x and y should be equal. (For objects, this is tested using the equals(...)
function. For primitives like int
, the ==
operator is used.)assertEquals(
x , y , delta)
: Floating point numbers x and y should be approximately equal, within plus or minus delta.assertNotEquals(
x , y)
: x and y should not be equal.assertSame(
obj1 , obj2)
: obj1 and obj2 should hold the same address.assertNotSame(
obj1 , obj2)
: obj1 and obj2 should not hold the same address.assertNull(
obj1 )
: obj1 should be a null reference.assertNotNull(
obj1 )
: obj1 should not be a null reference.More specialized assertions:
assertArrayEquals(
array1 , array2)
: array1 and array2 should have the same length and corresponding elements in these arrays should be equal.assertLinesMatch(
expectedList , actualListOfStrings)
: compares a list of expected strings/regular expressions against a list of actual strings.assertIterablesEquals(
expectedContainer , actualContainer)
: compares two containers of objects to see if their contents are equal.“Meta-” assertions:
fail(
message )
: always failsassertThrows(
exception, Executable )
: asserts that code will throw an exceptionassertTimeout(
timeLimit , Executable)
: asserts that code will finish within a stated time.assertTimeoutPreemptively(
timeLimit , Executable)
: asserts that code will finish within a stated time, and kills the code if it doesn’t.In these, the Executable
is likely to be a Java lambda expression.
e.g.,
assertTimeout(Duration.ofSeconds(1),
() -> {longMailingList1.merge(longMailingList2);});
Each assertion can have an optional final argument of a string that will be printed when the assertion fails. For example,
Integer pos = search(arr, x);
assertNotNull (pos);
assertNotNull (pos, "Could not find x in arr");
Now let’s look at addContact
.
Remember the basic pattern:
We can carryout the first two steps pretty easily.
@Test
void testAddContact() {
Contact holmes = new Contact("Holmes", "21B Baker St.");
MailingList ml = new MailingList();
ml.addContact(holmes);
⋮
}
Again we look at out list of accessors:
After adding holmes
…
contains
should return true for “Holmes” but false for other names.getContact
should return a contact equal to holmes
when given “Holmes”, null for all other names.size
should be 1.equals
should return false when compared to a freshly-constructed list, true when compared to one containing only holmes
.hashCode
s should be equal whenever equals
is true. @Test
void testAddContact() {
Contact holmes = new Contact("Holmes", "21B Baker St.");
MailingList ml = new MailingList();
ml.addContact(holmes);
assertEquals(1, ml.size());
assertTrue(ml.contains("Holmes"));
assertFalse(ml.contains("Jones"));
assertEquals (holmes, ml.getContact("Holmes"));
assertNull (ml.getContact("Jones"));
assertNotEquals(ml, new MailingList());
MailingList ml2 = new MailingList();
ml2.addContact(holmes);
assertEquals (ml2, ml);
assertEquals (ml2.hashCode(), ml.hashCode());
assertTrue(ml.toString().contains("Holmes"));
}
Run this and see if we pass.
Of course, adding a single contact to an empty mailing list is not much of a test.
We could make our existing test case for addContact
longer, or we can add additional cases to explore these possibilities:
@Test
void testAddContactMulti() {
Contact holmes = new Contact("Holmes", "21B Baker St."); ➀
Contact adams = new Contact("Adams", "21 Pennsylvania Ave.");
Contact baker = new Contact("Baker", "Drury Ln.");
Contact charles = new Contact("Charles", "100 1st St.");
Contact dickens = new Contact("Dickens", "200 2nd St.");
Contact[] inputs = {dickens, adams, charles, holmes, baker}; ➁
MailingList ml = new MailingList();
for (int testSize = 1; testSize <= inputs.length; ++testSize) {
MailingList ml0 = (MailingList)ml.clone(); ➂
ml.addContact(inputs[testSize-1]);
assertEquals(testSize, ml.size());
for (int j = 0; j <= testSize-1; ++j) { ➃
assertTrue (ml.contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName()); ➄
assertEquals (inputs[j], ml.getContact(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertTrue(ml.toString().contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
}
for (int j = testSize+1; j < inputs.length; ++j) { ➅
assertFalse (ml.contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertNull (ml.getContact(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertFalse(ml.toString().contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
}
assertNotEquals(ml, ml0);
}
}
ml0
sets up the “preserve-and-compare” idiom.i
contacts should be in the mailing list.
i
ones should not be in the mailing list.The documentation for addContact
says that duplicates are not added. We should test for that as well:
@Test
void testAddDuplicateContact() {
Contact holmes = new Contact("Holmes", "21B Baker St."); ➀
Contact adams = new Contact("Adams", "21 Pennsylvania Ave.");
Contact baker = new Contact("Baker", "Drury Ln.");
Contact charles = new Contact("Charles", "100 1st St.");
Contact dickens = new Contact("Dickens", "200 2nd St.");
Contact[] inputs = {dickens, adams, charles, holmes, baker}; ➁
MailingList ml = new MailingList();
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
// Add a duplicate
ml.addContact(baker); ➂
assertEquals(ml0.size(), ml.size());
assertEquals(ml0, ml); ➃
assertEquals(ml0.hashCode(), ml.hashCode());
assertTrue (ml.contains(baker.getName()));
assertEquals (baker, ml.getContact(baker.getName()));
assertTrue(ml.toString().contains(baker.getName()));
}
Look at how much of that test was devoted to the setup, much of which was identical to the setup of the previous testAddContact
case. Looking ahead, we can guess that we will need much of the same setup for test cases for removeContact
as well.
A test fixture in unit testing is a reusable collection of data that provides support for running multiple tests.
In JUnit, we create test fixtures by
@BeforeEach
function, and@AfterEach
function.So, if we create this fixture near the top of the test class:
class TestMailingList {
Contact holmes;
Contact adams;
Contact baker;
Contact charles;
Contact dickens;
Contact adams2;
Contact[] inputs;
MailingList ml;
@BeforeEach
void setup() {
holmes = new Contact("Holmes", "21B baker St.");
adams = new Contact("Adams", "21 Pennsylvania Ave.");
baker = new Contact("Baker", "Drury Ln.");
charles = new Contact("Charles", "100 1st St.");
dickens = new Contact("Dickens", "200 2nd St.");
adams2 = new Contact("Adams", "32 Pennsylvania Ave.");
Contact[] order = {dickens, adams, charles, holmes, baker};
inputs = order;
ml = new MailingList();
}
Then we can simplify our tests:
@Test
void testMailingList() {
assertEquals (0, ml.size());
assertFalse (ml.contains("Jones"));
assertNull (ml.getContact("Smith"));
MailingList ml2 = new MailingList();
assertEquals (ml2, ml);
assertEquals (ml2.hashCode(), ml.hashCode());
assertNotNull(ml.toString());
}
@Test
void testAddContact() {
ml.addContact(holmes);
assertEquals (1, ml.size());
assertTrue(ml.contains("Holmes"));
assertFalse(ml.contains("Jones"));
assertEquals (holmes, ml.getContact("Holmes"));
assertNull (ml.getContact("Jones"));
assertNotEquals(ml, new MailingList());
MailingList ml2 = new MailingList();
ml2.addContact(holmes);
assertEquals (ml2, ml);
assertEquals (ml2.hashCode(), ml.hashCode());
assertTrue(ml.toString().contains("Holmes"));
}
@Test
void testAddContactMulti() {
for (int testSize = 1; testSize <= inputs.length; ++testSize) {
MailingList ml0 = (MailingList)ml.clone();
ml.addContact(inputs[testSize-1]);
assertEquals(testSize, ml.size());
for (int j = 0; j <= testSize-1; ++j) {
assertTrue (ml.contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertEquals (inputs[j], ml.getContact(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertTrue(ml.toString().contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
}
for (int j = testSize+1; j < inputs.length; ++j) {
assertFalse (ml.contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertNull (ml.getContact(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertFalse(ml.toString().contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
}
assertNotEquals(ml, ml0);
}
}
@Test
void testAddDuplicateContact() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
// Add a duplicate
ml.addContact(baker);
assertEquals(ml0.size(), ml.size());
assertEquals(ml0, ml);
assertEquals(ml0.hashCode(), ml.hashCode());
assertTrue (ml.contains(baker.getName()));
assertEquals (baker, ml.getContact(baker.getName()));
assertTrue(ml.toString().contains(baker.getName()));
}
The documentation of addContact says that we are supposed to keep the contacts in ascending order.
That’s not a problem for addTestContact
, because we only have one contact (so it can hardly be out of order!). But it is something that we should be checking in the other two test cases.
But, how can we check that? Take another look at the interface of MailingList
. There’s nothing in there that enables us to access all of the contacts. We can only get a Contact is we already know its name. Surely that’s not a good design!
It’s very common to discover flaws in a class’ interface when trying to write the unit tests. That’s because each unit test represents a kind of demonstration of how to use the class’ interface.
If you can’t write a desired unit test, you probably can’t do what you want with the class in the application either.
As a matter of Java style, when a class holds a collection of smaller items, we provide access to those by adding an iterator to provide access to the contained items and making the container class Iterable
.
So, let’s modify the MailingList
class accordingly:
public class MailingList implements Cloneable, Iterable<Contact> {
private LinkedList<Contact> theContacts;
/**
* Create an empty mailing list
*
*/
public MailingList() {
theContacts = new LinkedList<Contact>();
}
⋮
/**
* Provide access to the contacts.
*/
@Override
public Iterator<Contact> iterator() {
return theContacts.iterator();
}
}
Now, we can write a test to express the idea that the contacts are kept in order:
@Test
void testAddDuplicateContact() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
// Add a duplicate
ml.addContact(baker);
assertEquals(ml0.size(), ml.size());
assertEquals(ml0, ml);
assertEquals(ml0.hashCode(), ml.hashCode());
assertTrue (ml.contains(baker.getName()));
assertEquals (baker, ml.getContact(baker.getName()));
assertTrue(ml.toString().contains(baker.getName()));
Contact[] expected = {adams, baker, charles, dickens, holmes};
int i = 0;
for (Contact contact: ml) {
assertEquals (expected[i], contact);
++i;
}
}
We’ll start with testAddDuplicateContact
because it’s a bit simpler.
expected
array indicates the order in which we expect to see the various contacts.expected
.Because this kind of comparison is so common, JUnit has an assertion specifically for that purpose:
@Test
void testAddDuplicateContact() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
// Add a duplicate
ml.addContact(baker);
assertEquals(ml0.size(), ml.size());
assertEquals(ml0, ml);
assertEquals(ml0.hashCode(), ml.hashCode());
assertTrue (ml.contains(baker.getName()));
assertEquals (baker, ml.getContact(baker.getName()));
assertTrue(ml.toString().contains(baker.getName()));
Contact[] expected = {adams, baker, charles, dickens, holmes};
assertIterableEquals(Arrays.asList(expected), ml);
}
Arrays.asList
call converts an ordinary array into something Iterable
, which can then be compared directly against the Iterable
variable ml
.With that, we can then add a similar ordering test to the more complicated testAddContactMulti
:
@Test
void testAddContactMulti() {
Contact[][] expected = {{dickens},
{adams, dickens},
{adams, charles, dickens},
{adams, charles, dickens, holmes},
{adams, baker, charles, dickens, holmes}
};
for (int testSize = 1; testSize <= inputs.length; ++testSize) {
MailingList ml0 = (MailingList)ml.clone();
ml.addContact(inputs[testSize-1]);
assertEquals(testSize, ml.size());
for (int j = 0; j <= testSize-1; ++j) {
assertTrue (ml.contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertEquals (inputs[j], ml.getContact(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertTrue(ml.toString().contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
}
for (int j = testSize+1; j < inputs.length; ++j) {
assertFalse (ml.contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertNull (ml.getContact(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
assertFalse(ml.toString().contains(inputs[j].getName()),
"failed after adding " + inputs[testSize-1].getName());
}
assertNotEquals(ml, ml0);
assertIterableEquals(Arrays.asList(expected[i]), ml);
}
}
i
elements from the input
, we check the mailing list against expected[i]
.That last test was a bit clunky because it was actually running a whole series of tests on mailingLists that had 1, 2, …, 5 contacts added to them. We can simplify this by using a facility called parameterized tests, which allow us to run a test repeatedly passing a different parameter to the test function each time:
@ParameterizedTest ➀
@ValueSource(ints = {1, 2, 3, 4, 5}) ➁
void testAddContactMulti(int testSize) { ➂
Contact[][] expected = {{dickens},
{adams, dickens},
{adams, charles, dickens},
{adams, charles, dickens, holmes},
{adams, baker, charles, dickens, holmes}
};
MailingList ml0 = (MailingList)ml.clone();
for (int i = 0; i < testSize; ++i) { ➃
ml.addContact(inputs[i]);
}
assertEquals(testSize, ml.size());
for (int j = 0; j <= testSize-1; ++j) {
assertTrue (ml.contains(inputs[j].getName()));
assertEquals (inputs[j], ml.getContact(inputs[j].getName()));
assertTrue(ml.toString().contains(inputs[j].getName()));
}
for (int j = testSize; j < inputs.length; ++j) {
assertFalse (ml.contains(inputs[j].getName()));
assertNull (ml.getContact(inputs[j].getName()));
assertFalse(ml.toString().contains(inputs[j].getName()));
}
assertNotEquals(ml, ml0);
assertIterableEquals(Arrays.asList(expected[testSize-1]), ml);
}
@Test
, we introduce this with @ParameterizedTest
.@ValueSource
is probably the easiest one to use. It allows us to supply a stream of “basic” values (ints, strings, etc.).
@MethodSource
allows you to supply a function that generates a series of values of any type you like.@Test
functions, which never take parameters.)Parameterized tests can be useful in introducing boundary and special values testing into our test cases.
Our test for the clone()
function starts much like you would expect it to:
@Test
void testClone() {
MailingList ml0 = new MailingList(); ➀
for (int i = 0; i < inputs.length; ++i) {
ml0.addContact(inputs[i]);
}
MailingList ml = (MailingList)ml0.clone(); ➁
assertThat (ml.size(), is(ml0.size())); ➂
assertThat (ml, equalTo(ml0));
assertThat (ml.hashCode(), equalTo(ml0.hashCode()));
assertIterableEquals(ml, ml0);
assertThat (ml.toString(), equalTo(ml0.toString()));
}
clone
call.Checking for Deep Copy
But the documentation of the MailingList.clone
function indicates that it is supposed to carry out a “deep copy”. That means that an implementation like this:
⋮
/**
* Deep copy of the contacts.
*/
public Object clone() {
MailingList result = new MailingList();
result.contacts = contacts;
return result;
}
would not be acceptable, because any subsequent modifications of the clone would also alter the original, and vice-versa. So we should test to be sure that does not happen:
@Test
void testClone() {
MailingList ml0 = new MailingList();
for (int i = 0; i < inputs.length; ++i) {
ml0.addContact(inputs[i]);
}
MailingList ml = (MailingList)ml0.clone();
assertThat (ml.size(), is(ml0.size()));
assertThat (ml, equalTo(ml0));
assertThat (ml.hashCode(), equalTo(ml0.hashCode()));
assertIterableEquals(ml, ml0);
assertThat (ml.toString(), equalTo(ml0.toString()));
// clone() should be a deep copy, so changing one list should
// not affect the other.
int size = ml.size();
ml.removeContact(baker);
assertThat (ml0.size(), is(size));
assertThat (ml0, not(equalTo(ml)));
assertTrue (ml0.contains(baker.getName()));
assertThat (ml0.getContact(baker.getName()), equalTo(baker));
assertThat (ml0.toString(), not(equalTo(ml.toString())));
ml = (MailingList)ml0.clone();
ml0.removeContact(baker);
assertThat (ml.size(), is(size));
assertThat (ml, not(equalTo(ml0)));
assertTrue (ml.contains(baker.getName()));
assertThat (ml.getContact(baker.getName()), equalTo(baker));
assertThat (ml.toString(), not(equalTo(ml0.toString())));
}
ml
, and then check to be sure the original ml0
was unaffected.ml0
, and then check to be sure that that clone ml
was unaffected.We still need tests for removeContact
(both versions) and merge
. None of these require any ideas that we have not already examined, so I won’t go through them in detail. The full source code for this example is here:
package mailinglist;
/**
* A contact is a name and address.
* <p>
* For the purpose of this example, I have simplified matters
* a bit by making both of these components simple strings.
* In practice, we would expect Address, at least, to be a
* more structured type.
*
* @author zeil
*
*/
public class Contact
implements Cloneable, Comparable<Contact> {
private String theName;
private String theAddress;
/**
* Create a contact with empty name and address.
*
*/
public Contact ()
{
theName = "";
theAddress = "";
}
/**
* Create a contact
* @param nm name
* @param addr address
*/
public Contact (String nm, String addr)
{
theName = nm;
theAddress = addr;
}
/**
* Get the name of the contact
* @return the name
*/
public String getName()
{
return theName;
}
/**
* Change the name of the contact
* @param nm new name
*/
public void setName (String nm)
{
theName= nm;
}
/**
* Get the address of the contact
* @return the address
*/
public String getAddress()
{
return theAddress;
}
/**
* Change the address of the contact
* @param addr new address
*/
public void setAddress (String addr)
{
theAddress = addr;
}
/**
* True if the names and addresses are equal
*/
public boolean equals (Object right)
{
Contact r = (Contact)right;
return theName.equals(r.theName)
&& theAddress.equals(r.theAddress);
}
public int hashCode ()
{
return theName.hashCode() + 3 * theAddress.hashCode();
}
public String toString()
{
return theName + ": " + theAddress;
}
public Object clone()
{
return new Contact(theName, theAddress);
}
/**
* Compare this contact to another.
* Return value > 0 if this contact precedes the other,
* == 0 if the two are equal, and < 0 if this contact
* follows the other.
*/
public int compareTo (Contact c)
{
int nmcomp = theName.compareTo(c.theName);
if (nmcomp != 0)
return nmcomp;
else
return theAddress.compareTo(c.theAddress);
}
}
package mailinglist;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
/**
* A collection of names and addresses
*/
public class MailingList implements Cloneable, Iterable<Contact> {
private LinkedList<Contact> theContacts;
/**
* Create an empty mailing list
*
*/
public MailingList() {
theContacts = new LinkedList<Contact>();
}
/**
* Add a new contact to the list. Contacts should be kept in ascending
* order with no duplicates.
*
* @param contact new contact to add
*/
public void addContact(Contact contact) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.compareTo(contact);
if (compared > 0) {
it.previous();
it.add(contact);
return;
} else if (compared == 0) {
return;
}
}
theContacts.add(contact);
}
/**
* Remove one matching contact
* @param contact remove a contact equal to contact
*/
public void removeContact(Contact contact) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.compareTo(contact);
if (compared > 0) {
return;
} else if (compared == 0) {
it.remove();
return;
}
}
}
/**
* Remove a contact with the indicated name
* @param name name of contact to remove
*/
public void removeContact(String name) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.getName().compareTo(name);
if (compared > 0) {
return;
} else if (compared == 0) {
it.remove();
return;
}
}
}
/**
* Search for contacts
* @param name name to search for
* @return true if a contact with an equal name exists
*/
public boolean contains(String name) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.getName().compareTo(name);
if (compared > 0) {
return false;
} else if (compared == 0) {
return true;
}
}
return false;
}
/**
* Search for contacts
* @param name name to search for
* @return contact with that name if found, null if not found
*/
public Contact getContact(String name) {
ListIterator<Contact> it = theContacts.listIterator();
while (it.hasNext()) {
Contact c = it.next();
int compared = c.getName().compareTo(name);
if (compared > 0) {
return null;
} else if (compared == 0) {
return c;
}
}
return null;
}
/**
* combine two mailing lists
*
*/
public void merge(MailingList anotherList) {
// For a quick merge, we will loop around, checking the
// first item in each list, and always copying the smaller
// of the two items into result
LinkedList<Contact> result = new LinkedList<Contact>();
ListIterator<Contact> thisPos = theContacts.listIterator();
ListIterator<Contact> otherPos = anotherList.theContacts.listIterator();
while (thisPos.hasNext() && otherPos.hasNext()) {
Contact thisContact = thisPos.next();
Contact otherContact = otherPos.next();
int comp = thisContact.compareTo(otherContact);
if (comp == 0) {
result.add(thisContact);
} else if (comp < 0) {
result.add(thisContact);
otherPos.previous();
} else {
result.add(otherContact);
thisPos.previous();
}
}
// Now, one of the two lists has been entirely copied.
// The other might still have stuff to copy. So we just copy
// any remaining items from the two lists. Note that one of these
// two loops will execute zero times.
while (thisPos.hasNext()) {
result.add(thisPos.next());
}
while (otherPos.hasNext()) {
result.add(otherPos.next());
}
// Now result contains the merged list. Transfer that into this list.
theContacts = result;
}
/**
* How many contacts in list?
*/
public int size() {
return theContacts.size();
}
/**
* Return true if mailing lists contain equal contacts
*/
public boolean equals(Object anotherList) {
if (!(anotherList instanceof MailingList)) {
return false;
}
MailingList right = (MailingList) anotherList;
if (size() != right.size()) { // (easy test first!)
return false;
}
ListIterator<Contact> thisPos = theContacts.listIterator();
ListIterator<Contact> otherPos = right.theContacts.listIterator();
while (thisPos.hasNext() && otherPos.hasNext()) {
Contact thisContact = thisPos.next();
Contact otherContact = otherPos.next();
if (!thisContact.equals(otherContact)) {
return false;
}
}
return true;
}
public int hashCode() {
return theContacts.hashCode();
}
public String toString() {
StringBuffer buf = new StringBuffer("{");
boolean first = true;
for (Contact c: theContacts) {
if (!first) {
buf.append(", ");
}
first = false;
buf.append(c.toString());
}
buf.append("}");
return buf.toString();
}
/**
* Deep copy of contacts
*/
public Object clone() {
MailingList result = new MailingList();
for (Contact c: theContacts) {
result.theContacts.add((Contact)c.clone());
}
return result;
}
/**
* Provide access to the contacts.
*/
@Override
public Iterator<Contact> iterator() {
return theContacts.iterator();
}
}
package mailinglist;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.Arrays;
import org.hamcrest.core.IsNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
class TestMailingList {
Contact holmes;
Contact adams;
Contact baker;
Contact charles;
Contact dickens;
Contact adams2;
Contact[] inputs;
MailingList ml;
@BeforeEach
void setup() {
holmes = new Contact("Holmes", "21B baker St.");
adams = new Contact("Adams", "21 Pennsylvania Ave.");
baker = new Contact("Baker", "Drury Ln.");
charles = new Contact("Charles", "100 1st St.");
dickens = new Contact("Dickens", "200 2nd St.");
adams2 = new Contact("Adams", "32 Pennsylvania Ave.");
Contact[] order = {dickens, adams, charles, holmes, baker};
inputs = order;
ml = new MailingList();
}
@Test
void testMailingList() {
assertEquals (0, ml.size());
assertFalse (ml.contains("Jones"));
assertNull (ml.getContact("Smith"));
MailingList ml2 = new MailingList();
assertEquals (ml2, ml);
assertEquals (ml2.hashCode(), ml.hashCode());
assertNotNull(ml.toString());
}
@Test
void testAddContact() {
ml.addContact(holmes);
assertEquals (1, ml.size());
assertTrue(ml.contains("Holmes"));
assertFalse(ml.contains("Jones"));
assertEquals (holmes, ml.getContact("Holmes"));
assertNull (ml.getContact("Jones"));
assertNotEquals(ml, new MailingList());
MailingList ml2 = new MailingList();
ml2.addContact(holmes);
assertEquals (ml2, ml);
assertEquals (ml2.hashCode(), ml.hashCode());
assertTrue(ml.toString().contains("Holmes"));
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testAddContactMulti(int testSize) {
Contact[][] expected = {{dickens},
{adams, dickens},
{adams, charles, dickens},
{adams, charles, dickens, holmes},
{adams, baker, charles, dickens, holmes}
};
MailingList ml0 = (MailingList)ml.clone();
for (int i = 0; i < testSize; ++i) {
ml.addContact(inputs[i]);
}
assertEquals(testSize, ml.size());
for (int j = 0; j <= testSize-1; ++j) {
assertTrue (ml.contains(inputs[j].getName()));
assertEquals (inputs[j], ml.getContact(inputs[j].getName()));
assertTrue(ml.toString().contains(inputs[j].getName()));
}
for (int j = testSize; j < inputs.length; ++j) {
assertFalse (ml.contains(inputs[j].getName()));
assertNull (ml.getContact(inputs[j].getName()));
assertFalse(ml.toString().contains(inputs[j].getName()));
}
assertNotEquals(ml, ml0);
assertIterableEquals(Arrays.asList(expected[testSize-1]), ml);
}
@Test
void testAddDuplicateContact() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
// Add a duplicate
ml.addContact(baker);
assertThat(ml.size(), equalTo(ml0.size()));
assertThat(ml, is(ml0)); // is() == equalTo()
assertThat(ml.hashCode(), equalTo(ml0.hashCode()));
assertTrue (ml.contains(baker.getName()));
assertThat (ml.getContact(baker.getName()), equalTo(baker));
assertTrue(ml.toString().contains(baker.getName()));
Contact[] expected = {adams, baker, charles, dickens, holmes};
assertThat(ml, contains(expected));
}
@Test
void testRemoveContactContact() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
ml.removeContact(charles);
Contact[] expected = {adams, baker, dickens, holmes};
assertThat (ml.size(), is(ml0.size()-1));
assertThat (ml, not(equalTo(ml0)));
assertFalse (ml.contains(charles.getName()));
assertNull (ml.getContact(charles.getName()));
assertThat (ml, contains(expected));
}
@Test
void testRemoveContactString() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml.clone();
ml.removeContact(dickens.getName());
Contact[] expected = {adams, baker, charles, holmes};
assertThat (ml.size(), is(ml0.size()-1));
assertThat (ml, not(equalTo(ml0)));
assertFalse (ml.contains(dickens.getName()));
assertNull (ml.getContact(dickens.getName()));
assertThat (ml, contains(expected));
}
@Test
void testMerge() {
for (int i = 0; i < inputs.length; ++i) {
ml.addContact(inputs[i]);
}
int divider = inputs.length / 2;
MailingList ml1 = new MailingList();
for (int i = 0; i <= divider; ++i) {
ml1.addContact(inputs[i]);
}
MailingList ml2 = new MailingList();
for (int i = divider; i < inputs.length; ++i) {
ml2.addContact(inputs[i]);
}
MailingList ml0 = (MailingList)ml2.clone();
ml1.merge(ml2);
assertThat (ml1.size(), is(ml.size()));
assertThat (ml1, equalTo(ml));
assertThat (ml1.hashCode(), equalTo(ml.hashCode()));
assertIterableEquals(ml1, ml);
assertThat (ml1.toString(), equalTo(ml.toString()));
assertThat (ml2.size(), is(ml0.size()));
assertThat (ml2, equalTo(ml0));
assertIterableEquals(ml2, ml0);
for (int i = 0; i < inputs.length; ++i) {
assertTrue (ml1.contains(inputs[i].getName()));
assertThat (ml1.getContact(inputs[i].getName()), is(inputs[i]));
}
}
@Test
void testClone() {
MailingList ml0 = new MailingList();
for (int i = 0; i < inputs.length; ++i) {
ml0.addContact(inputs[i]);
}
MailingList ml = (MailingList)ml0.clone();
assertThat (ml.size(), is(ml0.size()));
assertThat (ml, equalTo(ml0));
assertThat (ml.hashCode(), equalTo(ml0.hashCode()));
assertIterableEquals(ml, ml0);
assertThat (ml.toString(), equalTo(ml0.toString()));
// clone() should be a deep copy, so changing one list should
// not affect the other.
int size = ml.size();
ml.removeContact(baker);
assertThat (ml0.size(), is(size));
assertThat (ml0, not(equalTo(ml)));
assertTrue (ml0.contains(baker.getName()));
assertThat (ml0.getContact(baker.getName()), equalTo(baker));
assertThat (ml0.toString(), not(equalTo(ml.toString())));
ml = (MailingList)ml0.clone();
ml0.removeContact(baker);
assertThat (ml.size(), is(size));
assertThat (ml, not(equalTo(ml0)));
assertTrue (ml.contains(baker.getName()));
assertThat (ml.getContact(baker.getName()), equalTo(baker));
assertThat (ml.toString(), not(equalTo(ml0.toString())));
}
}
We’ve illustrated here the mutator/accessor coverage introduced earlier.
Many, perhaps most, ADTs provide a small number of operations that manipulate the data in a non-trivial fashion, and a large number of get/set attribute functions that, conceptually at least, simply store and retrieve a private data member.
For example, suppose we wanted to test the following ADT:
package mailinglist;
/**
* A contact is a name and address.
* <p>
* For the purpose of this example, I have simplified matters
* a bit by making both of these components simple strings.
* In practice, we would expect Address, at least, to be a
* more structured type.
*
* @author zeil
*
*/
public class Address implements Cloneable {
private String name;
private String streetAddress;
private String city;
private String state;
private String zipCode;
/**
* Create an address with all empty fields.
*
*/
public Address ()
{
name = "";
streetAddress = "";
city = "";
state = "";
zipCode = "";
}
/**
* Create an address.
*/
public Address (String nm, String streetAddr, String city,
String state, String zip)
{
name = nm;
streetAddress = streetAddr;
this.city = city;
this.state = state;
zipCode = zip;
}
/**
* @return the theName
*/
public String getName() {
return name;
}
/**
* @param theName the theName to set
*/
public void setName(String theName) {
this.name = theName;
}
/**
* @return the streetAddress
*/
public String getStreetAddress() {
return streetAddress;
}
/**
* @param streetAddress the streetAddress to set
*/
public void setStreetAddress(String streetAddress) {
this.streetAddress = streetAddress;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
/**
* @return the state
*/
public String getState() {
return state;
}
/**
* @param state the state to set
*/
public void setState(String state) {
this.state = state;
}
/**
* @return the zipCode
*/
public String getZipCode() {
return zipCode;
}
/**
* @param zipCode the zipCode to set
*/
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
/**
* True if the names and addresses are equal
*/
public boolean equals (Object right)
{
Address r = (Address)right;
return name.equals(r.name)
&& streetAddress.equals(r.streetAddress)
&& city.equals(r.city)
&& state.equals(r.state)
&& zipCode.equals(r.zipCode);
}
public int hashCode ()
{
return name.hashCode() + 3 * streetAddress.hashCode()
+ 5 * city.hashCode()
+ 7 * state.hashCode()
+ 11 * zipCode.hashCode();
}
public String toString()
{
return name + ": " + streetAddress + ": "
+ city + ", " + state + " " + zipCode;
}
public Object clone()
{
return new Address(name, streetAddress, city,
state, zipCode);
}
}
public class TestAddress {
final private String name0 = "John Doe";
final private String street0 = "221B Baker St.";
final private String city0 = "Podunk";
final private String state0 = "IL";
final private String zip0 = "01010";
⋮
@Test
public final void testSetCity() {
String city1 = "Norfolk"; ➀
Address addr0 = new Address(name0, street0, city0, state0, zip0);
Address addr1 = new Address(name0, street0, city0, state0, zip0);
addr1.setCity(city1); ➁
assertEquals (name0, addr1.getName()); ➂
assertEquals (street0, addr1.getStreetAddress());
assertEquals (city1, addr1.getCity());
assertEquals (state0, addr1.getState());
assertEquals (zip0, addr1.getZipCode());
assertFalse (addr1.equals(addr0));
assertTrue (addr1.toString().contains(city1));
}
This follows a pattern that should be increasingly familiar:
But there are a number of reasons why these “does not change” assertions are worth performing:
Our intuition is that a working implementation of setCity
won’t have any effect on the street address, zip code, etc.. But, if we knew that the code was working, we wouldn’t be testing it!
Our intuition that the behavior of “getCity
” has no bearing on the testing of “setCity
” is based on a misunderstanding of what the test functions are doing. Although we allocate one test function per mutator, each such function is not merely testing its associated mutator. It is testing both that mutator and all the accessors.