09 December 2011
In this post, I'll cover negative testing, which is to make sure we know what error conditions can occur and how to make sure our code can handle them appropriately. Let's say that I want to track our plebs in a registry as just a list of Plebeian
POJOs consisting of just a first and last name. I can load my registry from an InputStream
of newline-delimited String objects, each of which is itself a comma-delimited last and first names. Constructing the registry should throw either an exception if something went wrong or complete and able to return an unmodifiable list of Plebeian
objects.
But first, I'll be up front about this: the Apache Commons projects are freakin' awesome, especially Commons Lang and Commons IO. The reason they're so freakin' awesome is that they drastically reduce the amount of boilerplate code you need to write and add tons of one-liner convenience methods. I highly recommend their usage in your code when allowed not only for ease of use but to reduce the amount of code that needs to be tested. For this post I'll be using the following pieces of Commons functionality:
Validate.notNull
from Commons Lang for validating method parametersStringUtils.splitPreserveAllTokens
from Commons Lang for splitting a delimited String
without having to worry about Java's standard String.split
odditiesIOUtils.readLines
from Commons IO to easily read all lines from an InputStream
to a List<String>
My Plebeian
object is just a POJO with two fields: lastName
and firstName
. Here's my PlebeianRegistry
object:
public class PlebeianRegistry {
private final List<Plebeian> plebeians = new ArrayList<Plebeian>();
public PlebeianRegistry(final InputStream inputStream)
throws IOException {
Validate.notNull(inputStream, "InputStream parameter cannot be null");
final List<String> lines = IOUtils.readLines(inputStream);
for (final String line : lines) {
// get max of 2 delimited fields
final String[] fields = StringUtils.splitPreserveAllTokens(
line, ",", 2);
// only honor entries with two fields
if (fields.length == 2) {
final String lastName = fields[0];
final String firstName = fields[1];
plebeians.add(new Plebeian(lastName, firstName));
}
}
}
public List<Plebeian> getPlebeians() {
return Collections.unmodifiableList(plebeians);
}
}
There are a number of ways that constructing our registry of plebs could go wrong:
InputStream
parameter could be nullInputStream
could throw an exceptionAll of these behaviors signal errors that could occur when constructing the registry and because I want to know how I'm handling error conditions, I write negative tests that simulate cases where things do go wrong.
First, what happens when the caller passes null
to the PlebeianRegistry
constructor? I could just wait til the constructor tries to read from the InputStream
and then track down the line of code but if the reading is part of a big ol' line of complex code, it can be tricky to track down exactly what threw the NullPointerException
. And what if a bunch of other things happened before the read (such as opening, writing to, and closing a file) that can't easily be rolled back? It's usually best to exit as soon as we know something's wrong. To achieve this, the first thing the constructor does is verify that the InputStream
parameter is not null:
Validate.notNull(inputStream, "InputStream parameter cannot be null");
I get a helpful error message stating that the supplied parameter can't be null. This behavior is easily verified with the following test:
@Test(expected=NullPointerException.class)
public void nullInputStreamParameterShouldThrowException()
throws IOException {
InputStream inputStream = null;
new PlebeianRegistry(inputStream);
}
The @Test
annotation takes an optional parameter named expected
that tells jUnit that I expect a NullPointerException
to be thrown when passing a null InputStream
to the PlebeianRegistry
constructor. Alternatively, if I wanted to check the text of exception message, I could write the test as:
@Test
public void nullInputStreamParameterShouldThrowExceptionWithExpectedMessage()
throws IOException {
InputStream inputStream = null;
try {
new PlebeianRegistry(inputStream);
}
catch (NullPointerException e) {
assertThat(e.getMessage(), is("InputStream parameter cannot be null"));
}
}
Second, what happens when reading from the InputStream
throws an IOException
? This can occur for a variety of reasons, but how I get an InputStream
to throw an Exception
? By mocking one out via simple extension, of course! In this test, I give myself a little helper class that extends InputStream
and throws an IOException
whenever the read()
method is called:
class ExceptionThrowingInputStream extends InputStream {
@Override
public int read() throws IOException {
throw new IOException("This is a purposeful " +
"exception for negative testing");
}
}
Now I can just pass in an instance of my exception-throwing InputStream
to my PlebeianRegistry
constructor and test away:
@Test(expected=IOException.class)
public void badStreamShouldThrowException()
throws IOException {
InputStream inputStream = new ExceptionThrowingInputStream();
new PlebeianRegistry(inputStream);
}
All this test says is that when my InputStream
throws an IOException
, I expect to see it bubble up to the caller.
Third, what happens when a line from the InputStream
contains fewer or more than two fields? The data in the input could have been corrupted for some reason so only the last name appears or perhaps the plebs first name (the second field) contains a comma. To accommodate both conditions, I use the Commons Lang method StringUtils.splitPreserveAllTokens
passing the max number of tokens I want, followed up with a check on how many fields I actually got. I only want lines of input containing two fields and ignoring input lines containing one field and can do so with this test:
@Test
public void linesWithLessThan2FieldsShouldBeIgnored()
throws IOException {
String lineSeparator = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder();
sb.append("plebs last name").append(lineSeparator);
// use IOUtils to convert the string to an InputStream
InputStream inputStream = IOUtils.toInputStream(sb.toString());
PlebeianRegistry registry = new PlebeianRegistry(inputStream);
// the registry should be empty
assertThat(registry.getPlebeians().isEmpty(), is(true));
}
The registry was successfully constructed by is empty since the only line of input contained a single field and was ignored. Conversely, the plebs first name may actually contain a comma that we want to preserve:
@Test
public void linesWithMoreThan1CommaShouldTreatSecondAsPartOfFieldText()
throws IOException {
String lineSeparator = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder();
sb.append("harris,bill,schmoe").append(lineSeparator);
// use IOUtils to convert the string to an InputStream
InputStream inputStream = IOUtils.toInputStream(sb.toString());
PlebeianRegistry registry = new PlebeianRegistry(inputStream);
// there should be 1 Plebeian object in the registry
assertThat(registry.getPlebeians().size(), is(1));
Plebeian plebeian = registry.getPlebeians().get(0);
assertThat(plebeian.getLastName(), is("harris"));
assertThat(plebeian.getFirstName(), is("bill,schmoe"));
}
In the last line the test verifies that constructor preserved the second comma as part of the plebs first name.
For good measure, I threw in some extra tests that aren't necessarily negative tests but verify certain behaviors that I haven't covered in other posts to date:
InputStream
should be fully read by the constructorList
of Plebeian
objects should not be modifiableTesting the first is simply a matter of noting that in the Java API for read()
, -1 should be returned when the end of stream is reached:
@Test
public void linesWith2FieldsShouldBeHonoredAndInputFullyRead()
throws IOException {
String lineSeparator = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder();
sb.append("plebovitz,joe").append(lineSeparator);
sb.append("plebington,sam").append(lineSeparator);
// use IOUtils to convert the string to an InputStream
InputStream inputStream = IOUtils.toInputStream(sb.toString());
PlebeianRegistry registry = new PlebeianRegistry(inputStream);
// first assert that the entire stream has been read
// -1 is returned if there's nothing left to read
assertThat(inputStream.read(), is(-1));
// there should be 2 Plebeian objects in the registry
assertThat(registry.getPlebeians().size(), is(2));
}
The second behavior is that I don't want a caller constructing a PlebeianRegistry
to be able to modify the list, such as adding or removing Plebeian
objects or even clearing the entire list. To do so, note the getter for the list:
public List<Plebeian> getPlebeians() {
return Collections.unmodifiableList(plebeians);
}
Collections.unmodifiableList
is a simple wrapped provided by Java itself that throws UnsupportedOperationException
s whenever an attempt is made to modify the list. Note that it doesn't stop someone from modifying the objects within the list, only that the list itself cannot be added to or removed from. Testing that the list returned is not modifiable is easy with this test:
@Test(expected=UnsupportedOperationException.class)
public void listOfPlebeiansShouldNotBeModifiable()
throws IOException {
String lineSeparator = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder();
sb.append("pleb,joe").append(lineSeparator);
// use IOUtils to convert the string to an InputStream
InputStream inputStream = IOUtils.toInputStream(sb.toString());
PlebeianRegistry registry = new PlebeianRegistry(inputStream);
registry.getPlebeians().clear();
}
An effect of the expected
annotation is that the test only says that an UnsupportedOperationException
is thrown somewhere in the code, not exactly which line threw it. If I wanted to be absolutely certain of the line that the UnsupportedOperationException
was thrown, I'd have to write the test using fail()
:
@Test
public void listOfPlebeiansShouldNotBeModifiable2()
throws IOException {
String lineSeparator = System.getProperty("line.separator");
StringBuilder sb = new StringBuilder();
sb.append("pleb,joe").append(lineSeparator);
// use IOUtils to convert the string to an InputStream
InputStream inputStream = IOUtils.toInputStream(sb.toString());
PlebeianRegistry registry = new PlebeianRegistry(inputStream);
try {
registry.getPlebeians().clear();
fail("An UnsupportedOperationException should've been thrown");
}
catch (UnsupportedOperationException e) {}
}
Surrounding the call to clear()
in a try-catch block allows the UnsupportedOperationException
to be caught and handled before the fail()
has a chance to fail the test.
Negative testing is a very aspect part of test-driven development and can get dramatically more involved than these simple examples but doing so can get you closer to 100% coverage and very important to verifying business requirements.