Columns

Why won't it work?

This month, I got a new laptop, wireless card and wireless access point, and connected everything to my existing router. Do you think all of this stuff worked right out of the box? If you said ''no,'' you're right. While my laptop worked, my next task was to get the latest drivers for everything else. Even with the new drivers in hand, access to the Web and e-mail was nothing but a dream. I also needed firmware upgrades in the router and access point, and the mixing and matching of driver versions between the OS and the card. I got everything to work, but only because I knew what I was doing -- I think. But what about the average consumer that just wants computing technology at home?

This episode got me thinking about software quality -- no surprise since I am teaching a course next term on the topic. This issue always seems to boil down to complexity and testing. How can one be sure that they have tested everything in their products, when one never knows how people are going to connect them to different devices from different vendors? The small challenge from Robert Binder's book, Testing Object-Oriented Systems (Reading, Mass.: Addison-Wesley, 2000), illustrates this problem precisely. His exercise was adapted from Glenford Myers' The Art of Software Testing (New York: Wiley, 1979) to devise a test plan for a simple program that ''reads three integer values from a card. The three values are interpreted as representing the lengths of the sides of a triangle. The program prints a message that states whether the triangle is scalene, isosceles, or equilateral.''

Binder's exercise proposed a Java class Triangle, subclassed from a Java class Figure. Figure supported a raster (pixel array) display, and Triangle supported methods is_scalene, is_isosceles and is_equilateral, each returning ''true'' or ''false'' depending on the state of the Triangle object. The exercise was to develop test cases that would ''adequately'' test Triangle. To help, Binder added the following to help define a Triangle:

''A valid triangle must meet two conditions. No side may have a length of zero and each side must be shorter than the sum of all sides divided by 2. That is, let a, b, and c be the sides of any triangle:

S = (a + b + c) / 2
Then s > a, s > b, and s > c must hold.

All sides are of equal length in an equilateral triangle. Any two sides are equal in an isosceles triangle, and all sides are unequal in a scalene triangle.''

How many test cases can you come up with? Myers came up with 24, although his problem didn't contain any object-specific cases. Binder came up with 65. His additional test cases reflected some of the unique factors of object-oriented software and testing: 1) Methods must be tested in the context of inherited features; 2) Because objects preserve state, one must test for bugs appearing in different message sequences; 3) Because objects are encapsulated, one must test for results of persistent values; and 4) Dynamic binding allows an object to produce many distinct and different behaviors. So how did you do? Remember, 65 test cases were for one class. Imagine testing an app that has hundreds of interacting classes. Is it no wonder nothing works, or are developers and testers to blame?

One important aspect of testing is test automation, which automates any aspect of testing an application system. One example of test automation software is JUnit (www.junit.org), a simple framework to write repeatable tests. It defines how to structure test cases and provides the tools to run them.

To work with JUnit, you implement a test in a subclass of TestCase. So, to test a class Triangle, you implement a class called TriangleTest as a subclass of TestCase. Because Java organizes classes into packages, one practice is to put TriangleTest in the same package as the classes under test, letting the test cases access the package private methods. Next, add a test method, like testAllSidesZero, that tests for an invalid response when all lengths of the sides of the Triangle are zero. A JUnit test method is an ordinary method without parameters that creates objects that interact during the test and verifies the result.

public class TriangleTest extends TestCase {
 //...
 public void testAllSidesZero() {
  Triangle triangle = new Triangle(0.0, 0.0, 0.0);)
  Assert.assertFalse(triangle.valid());
 }
}

Two additional steps are needed to run the test cases. You need to define how to run an individual test case and how to run a test suite. JUnit supports both static and dynamic running of single tests. In the static case, just override the runTest method inherited from TestCase and call the desired test case. One way to do this is with an anonymous inner class:

TestCase test= new TriangleTest(''all sides zero'') {
 public void runTest() {
  testAllSidesZero ();
 }
};

The dynamic way to create a test case and use reflection is to implement runTest. It assumes the name of the test is the name of the test case method to invoke. It dynamically finds and invokes the test method. To invoke the testAllSidesZero test we therefore need to construct a TriangleTest:

TestCase test= new TriangleTest(''testAllSidesZero'');

Naturally, you will have more than one test. To do this, you have to define a test suite. This is done by creating a static method called suite that is specialized to run tests. Inside suite you add the tests to be run to a TestSuite object and return it. A TestSuite can run a collection of tests. TestSuite and TestCase both implement an interface called Test, which defines the methods to run a test. This enables the creation of test suites by composing arbitrary TestCases and TestSuites.

public static Test suite() {
 TestSuite suite= new TestSuite();
 suite.addTest(new Triangle(''testAllSidesZero''));
 suite.addTest(new Triangle (''testAllSidesEqual''));
 return suite;
}

Now you are ready to run your tests. JUnit has a GUI called TestRunner to run tests. Type the name of your test class in the field at the top of the window and press the ''Run'' button. While the test is running, you will see the progress with a progress bar below the input field. The bar is initially green but turns to red as soon as there is an unsuccessful test. Failed tests are shown in a list at the bottom.

To make life even easier, JUnit has been integrated into Eclipse (www.eclipse.org). Under Eclipse's window menu, select show view > other > Java > JUnit. What you get is an Eclipse view that is its version of JUnit's TestRunner. To run a JUnit test, select the test class and do one of the following:

1. From the menu bar, select Run > Run as > JUnit Test.
2. On the Workbench tool bar, press the 'Run' button and choose Run as > JUnit Test

So what's the point? We have to get our software to work. Not just for those knee-deep in the technology, but for the average consumer who pays the bills and our salaries. One way to help with quality is to test. Testing should not start with the test team, but should start with the developer. JUnit is an excellent way for developers to automate their testing, resulting in test case classes that can be used later by the test team. Sound like too much work? Eclipse has made it easy to run your tests by integrating JUnit into its Java perspective. Test now, tomorrow and often. Don't waste anyone's time with your bugs when there is an easy way to make sure they don't happen.

About the Author

Dwight Deugo is a professor of computer science at Carleton University in Ottawa, Ontario. Dwight has been an editor for SIGS and 101communications publications, and serves as chair of the Java Programming track at the SIGS Conference for Java Development. He can be reached via e-mail at [email protected].