Columns
Why won't it work?
- By Dwight Deugo
- January 31, 2003
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].