Test Management Using JUnit


Test Management Using JUnit

The package JUnit is a tool used to perform unit tests , which are tests executed on a piece of code that test an individual piece of functionality. The problem with generating unit tests is that it takes time to generate the individual pieces of code. Therefore, many people do not end up writing any tests. This is a shame and leads to buggy code. Alternatively, some people do implement unit tests but write their own framework. That framework might not be comprehensive enough and will still lead to buggy code. The JUnit package is a framework used to write unit tests. The JUnit package does not write the tests for you, but it provides an infrastructure to write the tests.

Developers often wonder how many tests to write. Some people think that the design should be driven by the tests, whereas other people have other ideas. The opinion of the author is that tests should not drive the design of the software. Tests are used to validate design ideas.

The engineering industry and especially the car industry understand proper testing of designs. Consider a car. A car has thousands of parts . A car and its associated design are not driven by the tests because complete testing would result in a car that would not be affordable. Instead, the car designers rely on the car engineers and statistics to validate the quality of the car. Yes, as a result, recalls sometimes happen, and some people end up with lemons. But history has shown it is an effective way of weighing the costs of design and production versus the quality of the car.

The engineering world copes with how much testing has to be carried by complying with standards and defining contexts. For example, a water pump used to pump water to your lawn most likely will not have undergone that much testing. However, a water pump used to pump coolant liquids for the space shuttle will undergo much more testing. Even though the design of the water pump for the lawn and the shuttle is probably identical, the pump for the shuttle is absolutely critical. Of course, even the shuttle might suffer catastrophic failures; however, those failures are millions of factors lower than the failures of the water pump for the lawn. The difference in engineering levels is not comparable. Therefore, when constructing your own tests, consider the context. Will your software be executing as a water pump for the lawn or for the space shuttle? If you know which pump your software is, then you will also know how exhaustive your tests have to be. Remember also that the more tests there are, the less flexibly and more slowly your development will proceed. This is because you need time to digest the results of the various tests and to design new tests for new pieces of functionality.

Technical Details for JUnit

Tables 11.1 and 11.2 contain the abbreviated details necessary to use the JUnit package

Table 11.1: Repository details for the JUnit package.

Item

Details

Download repository

Details found at www.junit.org

Directory within repository

junit

Main packages used

junit.framework (the core classes and infrastructure); junit.awtui, junit.swingui, and junit.textui (contain the control programs that kickstart the individual tests)

Table 11.2: Package and class details of the JUnit toolkit.

Class/Interface

Details

junit.framework.TestCase

A neutral interface used to instantiate objects

junit.framework.TestSuite

The main class that contains all of the functions used to instantiate the different implementations of the Factory interface

[lang].functor.FactoryException

The exception thrown if the Factory classes have encountered a problem

Building a Simple Test Case

When you're developing test cases, the objective is to split the design into a main test program, modules that are tested , and then individual tests to be executed. Listing 11.1 is the main test program.

Listing 11.1
start example
 class MyRunner extends TestRunner { public void startTests() { TestSuite suite = new TestSuite(); suite.addTest( SampleTest.suite()); doRun(suite, false); } } public class ProjectManagement { public static void main(String args[]) { MyRunner runner = new MyRunner(); runner.startTests(); } } 
end example
 

In Listing 11.1, the class ProjectManagement is some main class that is executed when the program starts. The class ProjectManagement instantiates the class MyRunner . The class MyRunner subclasses the class TestRunner . In most source code examples of JUnit , very few cases subclass the class TestRunner . Instead, the class is instantiated directly. The reason for the subclassing is that it is easier to tweak certain parameters and settings (which will be shown later) than to do a direct instantiation.

In the implementation of the method startTests the class TestSuite is instantiated. The class TestSuite references all of the tests that will be executed, and monitors the individual progress of the tests. To add a test, the method addTest is called. The single parameter of the method addTest is a Test interface instance. Once all of the tests have been added, you can execute the tests by calling the method doRun . The method doRun has two parameters: suite and false . The parameter suite is the reference object instance to the suite of tests to run. The flag false indicates whether or not the test application should wait until after the tests have been executed.

Remember back to Chapter 9, where we discussed the concept of a functor. The functor had the ability to string together a number of classes that represent a single formula. The interface Test that was passed to the method addTest does the same thing as a functor, but instead strings tests that need to be executed together. In abstract terms, when a test is in fact a delegation to other tests, then that test is called a module . Listing 11.2 is the module test implementation.

Listing 11.2
start example
 public class SampleTest extends TestCase { public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest( SampleTestPiece.suite()); return suite; } } 
end example
 

In Listing 11.2, the class SampleTest extends the class TestCase . The class TestCase represents the test case infrastructure. The method suite returns a collection of tests that are added to the main test program in Listing 11.1. The class SampleTest is a module-level test and references other tests. The final individual tests are shown in Listing 11.3.

Listing 11.3
start example
 public class SampleTestPiece extends TestCase { public void testRoutine() throws Exception { System.out.println( "Something is tested"); } protected void setUp() throws Exception { System.out.println( "Setting up test"); } protected void tearDown() throws Exception { System.out.println( "Removing test resources"); } public static Test suite() { return new TestSuite( SampleTestPiece.class); } } 
end example
 

In Listing 11.3, the class SampleTestPiece like the module level class SampleTest extends the class TestCase . The difference with the SampleTestPiece class is that instead of performing another delegation to other tests, there are tests to execute. The method suite here is like the method suite in Listing 11.2, which adds a number of tests to the main test application. However, the difference is that the individual tests are extracted dynamically using the class TestSuite . The class TestSuite constructor parameter is a class descriptor to a specific class. The class TestSuite will dynamically iterate all of the methods of the class and extract the test methods. Test methods have the identifier test prepended to the method name , like the method testRoutine . To add a suite dynamically, you could have also used the class method TestSuite.addTestSuite .

The method setUp is an overridden method to initialize the class before the tests start. Some initialization operations could be the creation of database connections or the preloading of a configuration file. The method tearDown is also an overridden method, but it is used to undo and destroy the resources created in the method setUp .

Defining Tests Statically

In Listing 11.3, the tests routines were dynamically extracted. You can also statically define a test, as is shown in Listing 11.4.

Listing 11.4
start example
 public class AnotherTestPiece extends TestCase { public AnotherTestPiece( String test) { super( test); } public void testRoutine() throws Exception { System.out.println( "A test"); } public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest( new AnotherTestPiece( "testRoutine")); return suite; } } 
end example
 

In Listing 11.4, the class AnotherTestPiece has a single parameter constructor. The parameter is then passed to the base class TestCase . This is necessary because the parameter represents the identifier of the test. The individual test methods are added by calling the method addTest , with the instantiated AnotherTestPiece class. The parameter passed to the AnotherTestPiece class constructor is the name of the method that is supposed to be executed in the text.

Creating Individual Tests

Now that we've defined the technical details of setting up the test cases, our next topic is how to create a test. In Listing 11.4, the method testRoutine is used to execute some test. This test could be the instantiation of a class and then calling a method. If all goes well, then the test did its job. The focus of the test is to test an individual aspect of the program. The individual aspect could be whether two numbers were added correctly, or whether the end- user validated correctly. However, the tests test only one aspect, not multiple aspects. Listing 11.5 is a sample test.

Listing 11.5
start example
 public void testRoutine() throws Exception { BusinessLogic logic = new BusinessLogic(); assertTrue( "Could not add user", logic.addUser( "something")); } 
end example
 

In Listing 11.5, the class BusinessLogic is being tested. The specific aspect that is being tested is if the addUser method works properly. The method assertTrue is used to test if the method addUser worked successfully. There are two parameters to the method assertTrue . The first parameter is a message to output if the assert fails. The second parameter is an assert value. The method assertTrue was used, so a failure will be generated if the second parameter has a value of false .

The method assertTrue is part of the following set of methods to indicate the state of the tests:

  • assertTrue : Tests if the value of an assertion is true; if not, a failure is generated.

  • assertFalse : Tests if the value of an assertion is false; if not, a failure is generated.

  • assertEquals : A more complex assertion that has various overloaded implementations. The assertion tests if two objects equal each other. One of the two objects is the object to be tested and the other is a reference object determined to be correct. The two objects are compared using the equals method. If the two objects do not equal each other, then a failure is generated.

  • assertNotNull : Tests if the object reference value of an assertion is not null; if not, a failure is generated.

  • assertNull : Tests if the object reference value of an assertion is null; if not, a failure is generated.

  • assertSame : Tests if two objects are the same value. The two objects are compared using the equality operator. If the objects are not the same value, a failure is generated.

  • assertNotSame : Tests if two objects are the same value. The two objects are compared using the equality operator. If the objects are the same value, a failure is generated.

  • fail : A simple fail method, not an assertion. This method is useful when the test routine provides its own way of validating the state.

Writing test scripts is not that simple. In the case of Listing 11.5, it was easy because a simple operation was executed. However, in reality, testing means writing a sequence of test routines, not testing individual routines. Listing 11.6 is a sample implementation of a more complex scenario.

Listing 11.6
start example
 public class AnotherTestPiece extends TestCase { private User[] _users; private Mortgage _mortgage; boolean _isRuntime; public AnotherTestPiece( String test, boolean isRuntime) { super( test); _isRuntime = isRuntime; } protected void setUp() throws Exception { if( _isRuntime) { _users = Configuration.getAllUsers(); _mortgage = new Mortgage(); } } public void testConfigurationGeneration() throws Exception { User original = new User(); original.setFirstName( "christian"); original.setLastname( "gross"); StringWriter stringWriter = new StringWriter(); BeanWriter beanWriter = new BeanWriter(stringWriter); beanWriter.write(original); stringWriter.flush(); String xml = "<?xml version='1.0'?>" + stringWriter.toString(); BeanReader reader = new BeanReader(); reader.registerBeanClass( User.class ); User compareTo = (User) reader.parse( new StringReader(xml)); assertEquals( "Serialization does not work", original, compareTo); } public static Test suiteGeneration() { TestSuite suite= new TestSuite(); suite.addTest( new AnotherTestPiece( "testConfigurationGeneration", false)); return suite; } public void testAddUser() throws Exception { for( int c1 = 0; c1 < _users.length; c1 ++) { assertTrue( "Could not add user: " +  _users[ c1].toString(),  _mortgage.addUser( _users[ c1])); } } public void testRemoveUser() throws Exception { for( int c1 = 0; c1 < _users.length; c1 ++) { assertTrue( "Could not add user: " + _users[ c1].toString(), _mortgage.addUser( _users[ c1])); } } public static Test suiteRuntime() { TestSuite suite= new TestSuite(); suite.addTest( new AnotherTestPiece( "testAddUser", true)); suite.addTest( new AnotherTestPiece( "testRemoveUser", true)); return suite; } } 
end example
 

In Listing 11.6, the class AnotherTestPiece has been expanded to test two classes: User and Mortgage . The class User is a pure data class that contains data related to the identification of a particular user. The class Mortgage is an operations class used to process mortgages. Both of these classes have been dramatically abbreviated, even though it does not seem so, for illustration purposes.

In Listing 11.6, two static methods return a Test interface instance: suiteRuntime and suiteGeneration . There are two test methods because there are two sets of tests. Consider the problem that we need to create a comprehensive test framework that manipulates one user as well as multiple users. The easiest way to define multiple users is not by using code but by using configuration files. However, the problem of the configuration files is that tests have to be used to verify that configuration file routines work correctly. Think of it in the following way: how can tests be created when the testing routines might themselves have bugs ? The easiest way to ensure that the testing routines themselves do not have errors is to use serialization to generate a file populated with an initial test data set. The file is then copied and populated with different data. Granted, the different data also has to be tested, but at least the structure of the file containing the data set will not be buggy. The only test to write is the testing of the serialization routines that generate the initial data set.

The method suiteGeneration calls a single method testConfigurationGeneration . The method testConfigurationGeneration creates a single User class instance. The single User class instance is a serialization to an XML string, which is then serialized to an object instance. The original object instance original should be equal to the object instance compareTo . If you use the assertEquals method, a successful validation will generate no error. Therefore, you can generate a runtime test that reads in a number of serialized files to test the actual object implementations.

The generation validation is an important step. When you create tests for complex systems, the operations for initializing the objects need to be working correctly. If the initialization has bugs, then the runtime could either hide bugs or generate unexplainable ones.

The method suiteRuntime generates the tests for runtime validation. The runtime test procedures are testAddUser and testRemoveUser . The implementation of each of the runtime test procedures iterates the individual users and adds them to the mortgage or removes them from it. All of these methods are so-called business logic test routines. Typically, these kinds of methods will call multiple methods based on an initial and final state. What is tested is the final state using a series of asserts.

Notice in Listing 11.6 how the AnotherTestPiece constructor accepts two parameters. The second parameter is used to indicate in which mode the test process is executing. In Listing 11.6, the second parameter is a boolean , but it also could be a long value or an enumeration. The mode is required when you're setting up the object values in method setUp .

For the method setUp , a static class method, Configuration.getAllUsers is called. The method Configuration.getAllUsers is a routine that reads in the serialized user data generated by the generation routines. It is important to realize that any step in the method setUp cannot fail. An exception could be thrown to indicate incorrect user data. However, under no circumstances can an error be generated because the data is assumed correct. Of course, errors do occur. The objective is to minimize errors or the possibility of error.

Creating Tests That Fail

Generating test cases that report only failures is not a good idea because these tests describe only half the story. Typically, if an error occurs in good data, it either means that the data was wrong or something went wrong in the processing of the good data. Here is the problem: the premise that there could be serialization data errors has been established. If some of those errors are not caught, then the test cases will have bugs since bad data can be processed . Therefore, for all of the good tests, there need to be an equal number of bad tests. The bad tests make sure that all error handling routines are in place to catch bad data. The solution is to add another mode to the test cases. In Listing 11.6, there were two different implementations of the suite type method: suiteRuntime and suiteGeneration . We would have to add another suite type method for the bad data set. That mode would call a set of methods similar to the good data set, but the mode expects errors to be generated.




Applied Software Engineering Using Apache Jakarta Commons
Applied Software Engineering Using Apache Jakarta Commons (Charles River Media Computer Engineering)
ISBN: 1584502460
EAN: 2147483647
Year: 2002
Pages: 109

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net