|
Bugs are synonymous with software. Methods to minimize them out can be code-oriented approaches such as OO programming & exception handling, or can be test-oriented such as continuous build & test, automated testing, and unit test frameworks. Here’s an example highlighting the need for a unit test framework.
Suppose you want to write a math library that includes conversions, such as Fahrenheit-to-Celsius, feet-to-meters, etc. Some base functions could be celcius2fahrenheit( c ), feet2meters( f ), ... Along with this base code, you can write test procedures using a pre-defined data set as a sanity check for the conversions. In pseudocode,
celcius2fahrenheit( c ) return 1.8 * c + 32 feet2meters( f ) return f*.3048 celcius2fahrenheit_driver( c, expected ) if celcius2fahrenheit(c) == expected return True else print( "error in celcius2fahrenheit(), input = %f", c ) return False feet2meters_driver( f, expected ) if feet2meters(f) == expected return True else print( "error in feet2meters(), input = %f", c ) return False test_all() // boundary points make good test data celcius2fahrenheit_driver( 0,32 ); // freezing celcius2fahrenheit_driver( 100,212 ); // boiling celcius2fahrenheit_driver( 37 ,98.6 ); // body temp feet2meters_driver( 3, .9144 ); // 1 yard feet2meters_driver( 1, .3048 ); // 1 foot
Evolving to a framework
In this toy problem more code is dedicated to testing than the base functions itself, suggesting that the test code might introduce its own problems and be more trouble than it's worth. However, In full-blown programs, the base code will be more substantive, and test-code:base-code ratio will tip towards base code.
The simplistic approach above starts getting unwieldy when there are tens of base code functions and hundreds of data points. As off balanced as the test-code:base-code ratio is, our test code function "drivers" should do more. It should be informative when there’s an failure by printing out the callstack, handle exceptions, collect pass/fail statistics, etc. Now we begin to see a need for a framework library such as the Java's Junit, Python's PyUnit, .NET's NUnit -- more generally known as the xUnit famiy.
While the above abilities inflate the test code, language specific features can trim it down. One could use asserts or passing function pointers to generalize & consolidate the *_driver() definitions into a single function.
Organizing test code
In organizing the test code within the directory structure of the project code, one must balance two opposing forces:
# Primarily, we want to separate test from base code to prevent them from cluttering the typically very complex base code. # At the same time, we want to keep them together to easily maintain and synchronize them. If it's a small project, we can keep them in the same directory, or even in the same file.
Usually, our projects are complex so separating them is the best practice. One might keep a test directory tree that mirrors the tree of the base code. This mirror will help keep the test & base in sync, while separating them. The disadvantage is that one has to switch back-and-forth between trees while coding.
|
|
|