Life cycle of software engineering: requirements, analysis, design, implementation, test
Testing steps: unit, integration, validation, system, regression
Unit test: provide test case inputs to individual object, evaluate results for correctness
White box unit test: examine paths in sources to develop test cases
Black box unit test: examine specification of object to develop test cases
Test harness: outer program to provide structure to hold the object to be tested
JUnit: java test harness, implemented with Composite Pattern, to execute a test suite of test cases
JUnit is mostly focused on black box test
Methods assertEquals(), assertTrue() strategically placed to evaluate results for correctness
Assertions mainly test pre-conditions and post-conditions to method calls
Exceptions that arise from assertions are called ``failures'', and anticipated by the assertion
Other exceptions that are not anticipated (e.g. division by zero) are called ``errors''
Test suite of test cases can be used to ensure quality when code changes are made later (i.e. regression)
When a bug is reported, a JUnit test case can be developed to mimic the problem
Developer uses the test case, fixes the problem, and tries to get a clean run without a failure
Instead of implementation and then test case development: test-driven development
Write JUnit test cases first, using only stubs of designed objects, most will generate failures
Now start coding actual objects; finished coding when failures are eliminated
Composite Pattern: JUnit Testing [65]
Problem: How to build a hierarchy of objects such that a common method among all of the
objects can be invoked?
Solution: Composite Pattern
Outline: For example, the JUnit test harness requires that each test case of
a test suite is ``run''. Establish a Test interface with a run() method, implemented by individual
TestCases as well as TestSuite collections.
Build a hierarchical tree structure where interior
nodes are TestSuites, and leaf nodes are TestCases. Each TestSuite has a Vector of children,
which may be either another TestSuite or an individual TestCase. When the run() method is invoked
at the root (or any node) of the tree, then the run() method of all the children is invoked.
This propagates to the leaves of the tree.
All run() methods share a parameter, TestResult, which is a log of any failure or error exceptions. At
the end of the run, the log is printed to display the results.
In the example below, MyCalendarTest and VectorTest are TestCases, which are examined in more detail later.
Composite Pattern: Hierarchy [66]
TestSuite interior nodes have Vectors of many children, either TestSuites or TestCases
Invocation of the run() method at the root propagates to all the run() methods at the leaves
JUnit: Framework Java Code [67]
// Assert contains the methods used to test data for pre- and post-conditions
// An assertion which is not met causes an exception to be thrown
// Various signatures are provided to examine different types of data
package junit.framework;
public class Assert {
public static void assertTrue(String s, boolean condition) {
if (!condition)
throw new AssertionFailedError(s + ":" + condition);
}
public static void assertEquals(int expected, int actual) {
if (expected != actual)
throw new AssertionFailedError(expected + "<>" + actual);
}
public static void assertEquals(String expected, String actual) {
if (!expected.equals(actual))
throw new AssertionFailedError(expected + "<>" + actual);
}
}
// AssertionFailedError stores the error message for the exception
public class AssertionFailedError extends Error {
public AssertionFailedError() { super(); }
public AssertionFailedError(String s) { super(s); }
}
// All TestCases and TestSuites implement Test, hence a run() method, containing a log
public interface Test {
public void run(TestResult result);
}
// TestSuites contain Vectors of all their children, each an implementation of Test
// Children are added via addTest()
// Invocation of the run() method triggers invocation of the run() methods of all children
public class TestSuite implements Test {
private Vector fTests = new Vector();
public void run(TestResult result) {
for (Enumeration e = fTests.elements(); e.hasMoreElements();) {
Test test = (Test)e.nextElement();
test.run(result);
}
}
public void addTest(Test test) {
fTests.addElement(test);
}
}
// TestFailure encapsulates the Test which caused a Throwable (exception)
// Using reflection, toString() formats for display by the log
public class TestFailure {
private Test fFailedTest;
private Throwable fThrownException;
public TestFailure(Test test, Throwable t) {
fFailedTest = test;
fThrownException = t;
}
public Test failedTest() { return fFailedTest; }
public Throwable thrownException() { return fThrownException; }
public String toString() {
TestCase test = (TestCase)fFailedTest;
String className = test.getClass().getName();
return className + "." + test.getName() + "(): " + fThrownException.getMessage();
}
}
// TestResult is a log of all errors and failures
// print() iterates through both Vectors and the toString() above yields the display
public class TestResult {
protected Vector fErrors = new Vector();
protected Vector fFailures = new Vector();
public synchronized void addError(Test test, Throwable t) {
fErrors.addElement(new TestFailure(test, t));
}
public synchronized void addFailure(Test test, Throwable t) {
fFailures.addElement(new TestFailure(test, t));
}
public synchronized Enumeration errors() {
return fErrors.elements();
}
public synchronized Enumeration failures() {
return fFailures.elements();
}
public synchronized void print() {
System.out.println("Errors:");
for (int i=0; i<fErrors.size(); i++) {
TestFailure testFailure = (TestFailure)fErrors.elementAt(i);
System.out.println(testFailure);
}
System.out.println("Failures:");
for (int i=0; i<fFailures.size(); i++) {
TestFailure testFailure = (TestFailure)fFailures.elementAt(i);
System.out.println(testFailure);
}
}
}
// A test case extends TestCase, hence inherits assertions and must have a run() method
// The test case is run and exceptions caught
// The exceptions (anticipated AssertFailedErrors or other unanticipated Errors) are logged
// The method String name (with assert code), is converted to a Method object
// Then invoke() is called on the Method object
public abstract class TestCase extends Assert implements Test {
private final String fName;
public TestCase(String name) { fName = name; }
public String getName() { return fName; }
public void run(TestResult result) {
setUp();
try {
runTest();
}
catch(AssertionFailedError e) {
result.addFailure(this,e);
}
catch(Throwable e) {
result.addError(this,e);
}
finally {
tearDown();
}
}
public TestResult run() {
TestResult result = new TestResult();
run(result);
return(result);
}
public void runTest() throws Throwable {
Method runMethod = null;
try {
runMethod = getClass().getMethod(fName, null);
} catch (NoSuchMethodException e) {
assertTrue("Method \""+fName+"\" not found",false);
}
try {
runMethod.invoke(this, null);
}
catch (InvocationTargetException e) {
e.fillInStackTrace();
throw e.getTargetException();
}
}
protected void setUp() {}
protected void tearDown() {}
}
JUnit: Sample Java Code [70]
Composite Pattern is used to integrate VectorTest, MyCalendarTest into a TestSuite
import junit.framework.*;
public class AllTests {
public static Test suite() {
TestSuite A = new TestSuite();
TestSuite B = new TestSuite();
TestSuite suite = new TestSuite();
suite.addTest(A);
suite.addTest(B);
// testSize(), testElementAt() are test methods on a Vector
A.addTest(new VectorTest("testSize"));
A.addTest(new VectorTest("testElementAt"));
// testGetName() is the test method on a calendar
B.addTest(new MyCalendarTest("testGetName"));
return suite;
}
public static void main(String args[]) {
Test allTests = suite();
TestResult result = new TestResult();
// one log is passed to all invocations of the run() method
allTests.run(result);
// after run, display log of exceptions
result.print();
}
}
JUnit: Sample Java Code [71]
// Simple calendar conversion of integer month to String month
public class MyCalendar {
public static String getName(int month) {
switch (month) {
case 1: return "January";
case 2: return "February";
case 3: return "March";
case 4: return "April";
case 5: return "May";
case 6: return "June";
case 7: return "July";
case 8: return "August";
case 9: return "Septmber"; // THIS IS NOT SPELLED CORRECTLY
case 10: return "October";
case 11: return "November";
case 12: return "December";
default: return "UNKNOWN";
}
}
}
// TestCase for MyCalendar using one method
import junit.framework.*;
public class MyCalendarTest extends TestCase {
public MyCalendarTest(String s) {
super(s);
}
public void testGetName() {
// A guess is that 9 is not converted correctly to September
// Local variable name is not required
// The assertion will generate a failure exception to the log
String name = MyCalendar.getName(9);
assertEquals(name,"September");
}
}
JUnit: Sample Java Code [72]
import junit.framework.*;
import java.util.Vector;
public class VectorTest extends TestCase {
private Vector v;
public VectorTest(String s) { super(s); }
// All TestCases have an optional setUp() before each invocation of individual tests
protected void setUp() {
v = new Vector();
// a new Vector should be emtpy
assertTrue("isEmpty",v.isEmpty());
// after adding an element, the Vector should NOT be empty
v.addElement(new Integer(1));
assertTrue("!isEmpty",!v.isEmpty());
v.addElement(new Integer(2));
v.addElement(new Integer(3));
}
// Each of these method's name was attached to the test case via AllTests.java
// This can be cumbersome and JUnit can automatically identify test methods
public void testSize() {
int size = v.size();
for (int i=0; i<100; i++)
v.addElement(new Integer(i));
// the size should have grown by 100 elements
assertEquals(v.size(), size+100);
}
public void testElementAt() {
// the original value added should be 1
Integer i= (Integer)v.elementAt(0);
assertEquals(i.intValue(),1);
}
}