Unit Test for Buddi: Difference between revisions
No edit summary |
No edit summary |
||
Line 117: | Line 117: | ||
Of course, from a human factors standpoint, you'd need to modify the code so that it's flexible enough to allow people to eat their ice cream first, if so desired. In which case, you'd need to add a test to prove that your four-year old nephew's ice cream comes with everyone else's salads, but Grandma's ice cream comes at the end with your cappuccino. | Of course, from a human factors standpoint, you'd need to modify the code so that it's flexible enough to allow people to eat their ice cream first, if so desired. In which case, you'd need to add a test to prove that your four-year old nephew's ice cream comes with everyone else's salads, but Grandma's ice cream comes at the end with your cappuccino. | ||
=== Range === | |||
Range is a convenient catch-all word for the situation where a variable's type allows it to take on a wider range of values than you need—or want. For instance, a person's age is typically represented as an integer, but no one has ever lived to be 200,000 years old, even though that's a perfectly valid integer value. Similarly, there are only 360 degrees in a circle, even though degrees are commonly stored in an integer. | |||
In good object oriented design, you do not use a raw native type (e.g., an int or Integer) to store a bounded-integer value such as an age, or a compass heading. | |||
Bearing.java | |||
/** | |||
* Compass bearing | |||
*/ | |||
public class Bearing { | |||
protected int bearing; // 0..359 | |||
/** | |||
* Initialize a bearing to a value from 0..359 | |||
*/ | |||
public Bearing(int num_degrees) { | |||
if (num_degrees < 0 || num_degrees > 359) { | |||
throw new RuntimeException("Bad bearing"); | |||
} | |||
bearing = num_degrees; | |||
} | |||
/** | |||
* Return the angle between our bearing and another. | |||
* May be negative. | |||
*/ | |||
public int angleBetween(Bearing anOther) { | |||
return bearing - anOther.bearing; | |||
} | |||
} | |||
Notice that the angle returned is just an int, as we are not placing any range restrictions on the result (it may be negative, etc.) | |||
By encapsulating the concept of a bearing within a class, you've now got one place in the system that can filter out bad data. You cannot create a Bearing object with out of range values. Thus, the rest of the system can use Bearing objects and be assured that they contain only reasonable values. | |||
Other ranges may not be as straightforward. For instance, suppose you have a class that maintains two sets of x, y coordinates. These are just integers, with arbitrary values, but the constraint on the range is such that the two points must describe a rectangle with no side greater than 100 units. That is, the allowed range of values for both x, y pairs is interdependent. You'll want a range test for any method that can affect a coordinate to ensure that the resulting range of the x, y pairs remains legitimate. For more information on this topic, see "invariants" in the Design Issues chapter on page 99. | |||
Since you will likely call this from a number of different tests, it probably makes sense to make a new assert method: | |||
public static final int MAX_DIST = 100; | |||
public void assertPairRange(String message, | |||
Point one, Point two) { | |||
assertTrue(message, | |||
Math.abs(one.x - two.x) <= MAX_DIST); | |||
assertTrue(message, | |||
Math.abs(one.y - two.y) <= MAX_DIST); | |||
} | |||
But the most common ranges you'll want to test probably depend on physical data structure issues, not application domain constraints. Take a simple example like a stack class that implements a stack of Strings using an array: | |||
MyStack.java | |||
public class MyStack { | |||
public MyStack() { | |||
stack = new String[100]; | |||
next_index = 0; | |||
} | |||
public String pop() { | |||
return stack[--next_index]; | |||
} | |||
// Delete n items from the stack en-masse | |||
public void delete(int n) { | |||
next_index -= n; | |||
} | |||
public void push(String aString) { | |||
stack[next_index++] = aString; | |||
} | |||
public String top() { | |||
return stack[next_index-1]; | |||
} | |||
private int next_index; | |||
private String[] stack; | |||
} | |||
There are some potential bugs lurking here, as there are no checks at all for either an empty stack or a stack overflow. However we manipulate the index variable next index,one thing is supposed to be always true: (next_index >= 0&& next index < stack.length). We'd like to check to make sure this expression is true. | |||
Both next_index and stack are private variables; you don't want to have to expose those just for the sake of testing. There are several ways around this problem; for now we'll just make a special method in MyStack named checkInvariant(): | |||
MyStack.java | |||
Public void checkInvariant() | |||
throws InvariantException { | |||
// JDK 1.4 can use assert() instead | |||
if ( !(next_index >= 0 && | |||
next_index < stack.length)) { | |||
throw new InvariantException( | |||
"next_index out of range: " + | |||
next_index + | |||
" for stack length " + | |||
stack.length); | |||
} | |||
} | |||
Now a test method can call checkInvariant() to ensure that nothing has gone awry inside the guts of the stack class, without having direct access to those same guts. | |||
TestMyStock.java | |||
import junit.framework.*; | |||
public class TestMyStack extends TestCase { | |||
public void testEmpty() throws InvariantException { | |||
MyStack stack = new MyStack(); | |||
stack.checkInvariant(); | |||
stack.push("sample"); | |||
stack.checkInvariant(); | |||
// Popping ;last element ok | |||
assertEquals("sample", stack.pop()); | |||
stack.checkInvariant(); | |||
// Delete from empty stack | |||
stack.delete(1); | |||
stack.checkInvariant(); | |||
} | |||
} | |||
When you run this test, you'll quickly see that we need to add some range checking! | |||
There was 1 error: | |||
1) testEmpty(TestMyStack)InvariantException: | |||
next_index out of range: -1 for stack length of 100 | |||
at MyStack.checkInvariant(MyStack.java:31) | |||
at TestMyStack.testEmpty(TestMyStack.java:16) | |||
It's much easier to find and fix this sort of error here in a simple testing environment instead of buried in a real application. | |||
Almost any indexing concept (whether it's a genuine integer index or not) should be extensively tested. Here are a few ideas to get you started: | |||
* | |||
Start and End index have the same value | |||
* | |||
First is greater than Last | |||
* | |||
Index is negative | |||
* | |||
Index is greater than allowed | |||
* | |||
Count doesn't match actual number of items | |||
* | |||
… |
Revision as of 00:02, 20 October 2009
Unit Test for Buddi
CORRECT Boundary Conditions
Many bugs in code occur around "boundary conditions," that is, under conditions where the code's behavior may be different from the normal, day-to-day routine.
For instance, suppose you have a function that takes two integers:
public int calculate(int a, int b) { return a/(a+b); }
Most of the time, this code will return a number just as you expect. But if the sum of a and b happens to equal zero, you will get an ArithmeticException instead of a return value. That is a boundary condition—a place where things might suddenly go wrong, or at least behave differently from your expectations.
To help you think of tests for boundary conditions, we'll use the acronym CORRECT:
- Conformance — Does the value conform to an expected format?
- Ordering — Is the set of values ordered or unordered as appropriate?
- Range — Is the value within reasonable minimum and maximum values?
- Reference — Does the code reference anything external that isn't under direct control of the code itself?
- Existence — Does the value exist (e.g., is non-null, non-zero, present in a set, etc.)?
- Cardinality — Are there exactly enough values?
- Time (absolute and relative) — Is everything happening in order? At the right time? In time?
Let's look at each one of these in turn. Remember that for each of these areas, you want to consider data that is passed in as arguments to your method as well as internal data that you maintain inside your method and class.
The underlying question that you want to answer fully is:
What else can go wrong?
Once you think of something that could go wrong, write a test for it. Once that test passes, again ask yourself, "what else can go wrong?" and write another test, and so on.
Conformance
Many times you expect or produce data that must conform to some specific format. An e-mail address, for instance, isn't just a simple string. You expect that it must be of the form:
*
name@somewhere.com
With the possibility of extra dotted parts:
*
firstname.lastname@subdomain.somewhere.com
And even oddballs like this one:
*
firstname.lastname%somewhere@subdomain.somewhere.com
Suppose you are writing a method that will extract the user's name from their e-mail address. You'll expect that the user's name is the portion before the "@" sign. What will your code do if there is no "@" sign? Will it work? Throw a runtime exception? Is this a boundary condition you need to consider?[1]
Validating formatted string data such as e-mail addresses, phone numbers, account numbers, or file names is usually straightforward. But what about more complex structured data? Suppose you are reading some sort of report data that contains a header record linked to some number of data records, and finally to a trailer record. How many conditions might we have to test?
*
What if there's no header, just data and a trailer? *
What if there's no data, just a header and trailer? *
What if there's no trailer, just a header and data? *
What if there's just a trailer? *
What if there's just a header? *
What if there's just data?
Just as with the simpler e-mail address example, you have to consider what will happen if the data does not conform to the structure you think it should.
And of course, if you are creating something like an e-mail address (possibly building it up from different sources) or the structured data above, you want to test your result to make sure it conforms.
[1]E-mail addresses are actually very complicated. A close reading of RFC822 may surprise you.
Ordering
Another area to consider is the order of data, or the position of one piece of data within a larger collection. For instance, in the largest() example in the previous chapter, one bug manifested itself depending on whether the largest number you were searching for was at the beginning or end of the list.
That's one aspect of ordering. Any kind of search routine should be tested for conditions where the search target is first or last, as many common bugs can be found that way.
For another aspect of ordering, suppose you are writing a method that is passed a collection containing a restaurant order. You would probably expect that the appetizers will appear first in the order, followed by the salad (and that all-important dressing choice), then the entree and finally a decadent dessert involving lots of chocolate.
What happens to your code if the dessert is first, and the entree is last?
If there's a chance that sort of thing can happen, and if it's the responsibility of your method to deal with it if it does, then you need to test for this condition and address the problem. Now, it may be that this is not something your method needs to worry about. Perhaps this needs to be addressed at the user input level (see "Testing Invalid Parameters" later on).
If you're writing a sort routine, what might happen if the set of data is already ordered? Or worse yet, sorted in precisely reverse order? Ask yourself if that could cause trouble—if these are conditions that might be worth testing, too.
If you are supposed to maintain something in order, check that it is. For example, if your method is part of the GUI that is sending the dinner order back to the kitchen, you should have a test that verifies that the items are in the correct serving order:
public void testKitchenOrder() {
Order order = new Order(); FoodItem dessert = new Dessert("Chocolate Souffle"); FoodItem entree = new Entree("Beef Oscar"); FoodItem salad = new Salad("Tossed", "Parmesan Peppercorn"); // Add out of order order.addFoodItem(dessert); order.addFoodItem(entree); order.addFoodItem(salad); // But should come out in serving order Iterator itr = order.iterator(); assertEquals(salad, itr.next()); assertEquals(entree, itr.next()); assertEquals(dessert, itr.next()); // No more left assertFalse(itr.hasNext());
}
Of course, from a human factors standpoint, you'd need to modify the code so that it's flexible enough to allow people to eat their ice cream first, if so desired. In which case, you'd need to add a test to prove that your four-year old nephew's ice cream comes with everyone else's salads, but Grandma's ice cream comes at the end with your cappuccino.
Range
Range is a convenient catch-all word for the situation where a variable's type allows it to take on a wider range of values than you need—or want. For instance, a person's age is typically represented as an integer, but no one has ever lived to be 200,000 years old, even though that's a perfectly valid integer value. Similarly, there are only 360 degrees in a circle, even though degrees are commonly stored in an integer.
In good object oriented design, you do not use a raw native type (e.g., an int or Integer) to store a bounded-integer value such as an age, or a compass heading.
Bearing.java
/**
* Compass bearing */
public class Bearing {
protected int bearing; // 0..359 /** * Initialize a bearing to a value from 0..359 */ public Bearing(int num_degrees) { if (num_degrees < 0 || num_degrees > 359) { throw new RuntimeException("Bad bearing"); } bearing = num_degrees; } /** * Return the angle between our bearing and another. * May be negative. */ public int angleBetween(Bearing anOther) { return bearing - anOther.bearing; }
}
Notice that the angle returned is just an int, as we are not placing any range restrictions on the result (it may be negative, etc.)
By encapsulating the concept of a bearing within a class, you've now got one place in the system that can filter out bad data. You cannot create a Bearing object with out of range values. Thus, the rest of the system can use Bearing objects and be assured that they contain only reasonable values.
Other ranges may not be as straightforward. For instance, suppose you have a class that maintains two sets of x, y coordinates. These are just integers, with arbitrary values, but the constraint on the range is such that the two points must describe a rectangle with no side greater than 100 units. That is, the allowed range of values for both x, y pairs is interdependent. You'll want a range test for any method that can affect a coordinate to ensure that the resulting range of the x, y pairs remains legitimate. For more information on this topic, see "invariants" in the Design Issues chapter on page 99.
Since you will likely call this from a number of different tests, it probably makes sense to make a new assert method:
public static final int MAX_DIST = 100;
public void assertPairRange(String message,
Point one, Point two) { assertTrue(message, Math.abs(one.x - two.x) <= MAX_DIST); assertTrue(message, Math.abs(one.y - two.y) <= MAX_DIST);
}
But the most common ranges you'll want to test probably depend on physical data structure issues, not application domain constraints. Take a simple example like a stack class that implements a stack of Strings using an array:
MyStack.java
public class MyStack {
public MyStack() { stack = new String[100]; next_index = 0; } public String pop() { return stack[--next_index]; } // Delete n items from the stack en-masse public void delete(int n) { next_index -= n; } public void push(String aString) { stack[next_index++] = aString; } public String top() { return stack[next_index-1]; }
private int next_index; private String[] stack;
}
There are some potential bugs lurking here, as there are no checks at all for either an empty stack or a stack overflow. However we manipulate the index variable next index,one thing is supposed to be always true: (next_index >= 0&& next index < stack.length). We'd like to check to make sure this expression is true.
Both next_index and stack are private variables; you don't want to have to expose those just for the sake of testing. There are several ways around this problem; for now we'll just make a special method in MyStack named checkInvariant():
MyStack.java
Public void checkInvariant()
throws InvariantException { // JDK 1.4 can use assert() instead if ( !(next_index >= 0 && next_index < stack.length)) { throw new InvariantException( "next_index out of range: " + next_index + " for stack length " + stack.length); }
}
Now a test method can call checkInvariant() to ensure that nothing has gone awry inside the guts of the stack class, without having direct access to those same guts.
TestMyStock.java
import junit.framework.*;
public class TestMyStack extends TestCase {
public void testEmpty() throws InvariantException { MyStack stack = new MyStack();
stack.checkInvariant(); stack.push("sample"); stack.checkInvariant(); // Popping ;last element ok assertEquals("sample", stack.pop()); stack.checkInvariant(); // Delete from empty stack stack.delete(1); stack.checkInvariant(); }
}
When you run this test, you'll quickly see that we need to add some range checking!
There was 1 error: 1) testEmpty(TestMyStack)InvariantException:
next_index out of range: -1 for stack length of 100 at MyStack.checkInvariant(MyStack.java:31) at TestMyStack.testEmpty(TestMyStack.java:16)
It's much easier to find and fix this sort of error here in a simple testing environment instead of buried in a real application.
Almost any indexing concept (whether it's a genuine integer index or not) should be extensively tested. Here are a few ideas to get you started:
*
Start and End index have the same value *
First is greater than Last *
Index is negative *
Index is greater than allowed *
Count doesn't match actual number of items *
…