6.2 Recipes for defining new tests

6.2 Recipes for defining new tests

The following recipes describe how to adapt the JUnit framework. This section presents the cookbook recipes for adapting the three main components of JUnit:

  • the definition of test cases

  • their combination into test suites

  • the adaptation of test reporting mechanisms.

Additionally, sample adaptations of JUnit demonstrate how to apply the cookbook recipes. The adaptations rely on test cases for a simple complex number class introduced in Example 6.4 and the UML-F diagram in Figure 6.7. It offers methods for accessing its attributes (the real and imaginary part of a complex number), as well as for adding and multiplying complex numbers.

Figure 6.7. The UML-F diagram of class ComplexNumber
graphics/06fig07.gif
Example 6.4 Java source code of class ComplexNumber
 class ComplexNumber {      private double fReal;      private double fImaginary;      public ComplexNumber(double re, double im) {           fReal = re;           fImaginary = im;      }      public double getReal() {           return fReal;      }      public double getImaginary() {           return fImaginary;      }      ...      public ComplexNumber add(ComplexNumber c) {            return new ComplexNumber (                getReal() + c.getReal(),                getImaginary() + c.getImaginary());      }      public ComplexNumber multiply(ComplexNumber c) {            double re = getReal()*c.getReal()                      getImaginary()*c.getImaginary();           double im = getImaginary()*c.getReal() +                           getReal()*c.getImaginary();           return new ComplexNumber(re, im);      }      public boolean equals(Object anObject) {           if (anObject instanceof ComplexNumber) {                ComplexNumber c = (     ComplexNumber)                                         anObject;                return ((c.getReal() == getReal()) &&                (c.getImaginary() == getImaginary()));           } else                 return false;      } } 

6.2.1 Recipe for creating automated tests in JUnit

The main purpose of JUnit is to facilitate the creation of automated tests for Java programs. This adaptation is therefore the most common one. The adaptation is straightforward, and we structure its description into four recipes: one composite recipe that provides an overview; and three basic recipes that refine the first one by explaining how to define test cases and how to group test cases into test suites. Table 6.2 shows the cookbook recipe 'How to create automated tests in JUnit'.

Table 6.2. Recipe providing an overview for adapting JUnit
Recipe 'JUnit 1: How to create automated tests in JUnit'
Intent To define test cases for Java applications.
Classes TestCase and TestSuite.
Related recipes
  • 'JUnit 1.1: How to define a test case', or

  • 'JUnit 1.1A: How to define test cases as inner classes' together with

  • 'JUnit 1.2: How to compose a test suite'.

Steps to Apply
  1. Understand the functionality to be tested and plan the tests that are needed.

  2. Apply the recipe 'JUnit 1.1: How to define a test case' to create the appropriate test cases. Repeat this step, as often as necessary, but you may also adapt previously defined tests to reuse parts for new tests.

  3. Alternatively to step 2, apply the more sophisticated recipe 'JUnit 1.1A: How to define test cases as inner classes' allowing the definition of several test cases in a single class file.

  4. Add the test cases to a given test suite or group them into a new test suite by applying the recipe 'JUnit 1.2: How to compose a test suite'.

Discussion For discussion of the details refer to the subrecipes.
Usually step 2 (creating a test) is applied repeatedly to gain a suite of tests. Typically, new tests are defined on the basis of older ones. In particular, setUp() and tearDown() methods are candidates for reuse. The alternative step 3 is more compact if a number of related tests has to be defined.

JUnit provides standard ways for reporting test results. Moreover, adaptation of the result reporting is done only once per project. Therefore, the adaptation of the test result reporting mechanism is addressed independently later in this chapter.

According to step 1 of the recipe, we have to figure out what has to be tested. We suggest at least one test for each calculation method. This might be useful, for example, if the underlying data structure changes in the future. Furthermore, the equals() method deserves several tests. Thus, the following may be an appropriate test plan:

  • One test for add, with arbitrary, non-zero data.

  • One test for multiply, with arbitrary, non-zero data.

  • Four tests for the equals method:

    1. Two equal numbers, represented by distinct objects;

    2. Two different numbers with a non-zero real part only;

    3. Two different numbers with a non-zero imaginary part only;

    4. The object passed as parameter for comparison is not a ComplexNumber.

The next section presents the recipes for test cases and test suites, so that we can adapt JUnit for the tests sketched above.

6.2.2 Cookbook recipe for the definition of a test case

Since the design of the TestCase variation point is based on the Unification construction principle (see Figure 6.2), its adaptation requires the creation of a subclass and the overriding of its hook methods ( Unif h ). As the run() method is the template method (UML F tag Unif t ) it should not be overridden. Table 6.3 describes the cookbook recipe for adapting the JUnit class TestCase. It augments the generic recipe for the Unification construction principle presented in Chapter 5 with JUnit-specific constraints.

Table 6.3. Cookbook recipe for defining a test case
Recipe 'JUnit 1.1: How to define a test case'
Intent To create a test case.
Classes TestCase.
Steps to Apply
  1. Decide what is to be tested, and choose an appropriate test setting. The following questions may help.

    • Which functionality is to be tested? A method call or an interaction between several related components?

    • What is the test data (fixture) to operate on?

    • What is the expected outcome?

  2. Is there similar code in test cases that can be reused? In particular, take a look at the setUp(),runTest(), and tearDown() methods.

  3. Define a new subclass, either as a direct subclass of TestCase or a subclass of some other appropriate subclass of TestCase.

  4. Define the attributes needed to hold the object structures (fixtures) needed for the test.

  5. Optional: override setUp(). The default implementation in TestCase is empty. If subclassing another test, setUp() may adapt the result of a super.setUp() call or reuse the inherited method.

  6. Override runTest() to implement the actual testing code.

  7. Optional: override tearDown() to clean up the system after the test (for example, to close files and other external connections). The default implementation is empty. If subclassing another test, tearDown() may adapt the result of a super.tearDown() call or just reuse the inherited method.

Discussion
  • Add each test case to a test suite as described in step 4 of recipe 'JUnit 1: How to create automated tests in JUnit'.

  • To allow reuse of setUp() through subclassing, we recommend defining test attributes with protected access.

  • On some occasions one might reuse the runTest() method on different fixtures through inheritance. This happens, for example, if the test checks an invariant (constraint of the object structure) that should be valid on all object structures.

As described in the recipe, overriding the runTest(), setUp(), and tearDown() methods is optional. Figure 6.8 illustrates five common adaptation scenarios. TestA, TestB, and TestC are the most common adaptations. TestD reuses the runTest() method of TestB and applies it to different setUp() and tearDown() methods, whereas TestE reuses the setUp() and tearDown() code of its superclass TestC but redefines the runTest() method.

Figure 6.8. Some adaptation options for TestCase subclasses
graphics/06fig08.gif

In practice, the existing adaptation variants are so manifold that the guidelines given in the recipe can often only discuss major variants. In addition to the adaptation options shown in Figure 6.8, a number of less likely variants of adaptations exist.

Earlier we identified some tests to be conducted on instances of class ComplexNumber. Example 6.5 shows the source code of one of these tests it defines three attributes to hold the fixtures, a setUp() method and the appropriate runTest() method. Figure 6.9 shows the corresponding UML-F class diagram.

Figure 6.9. ComplexTestAdd class structure
graphics/06fig09.gif
Example 6.5 Sample test case for class ComplexNumber
 public class ComplexTestAdd extends TestCase {           private ComplexNumber fOneZero;           private ComplexNumber fZeroOne;             private ComplexNumber fOneOne;             protected void setUp() {                fOneZero = new ComplexNumber(1, 0);                fZeroOne = new ComplexNumber(0, 1);                fOneOne = new ComplexNumber(1, 1);           }           public void runTest() {                ComplexNumber result =                           fOneZero.add(fZeroOne);                /* assert is provided by JUnit in the                Assert class, which is the super class of                                     TestCase */                assert(fOneOne.equals(result));           } } 

6.2.3 Definition of several test cases in one source code file

The previous recipe requires the definition of each test in a separate class. To keep the number of classes, and thus the number of source code files, small it would be useful to define each test case in an individual method, but several of them within one class. However, all these methods need different names, whereas the JUnit framework as introduced so far accepts test case implementations only with the name runTest().

JUnit allows the definition of several test cases within one class by applying the GoF Adapter design pattern (Gamma et al., 1995) to match the actual method name containing the test with the name runTest() and by relying on so-called anonymous inner classes. Java provides the concept of anonymous inner classes for defining anonymous subclasses and overriding specific methods, without having to provide explicit class names. From a modeling viewpoint, such an anonymous inner class is like a normal class except that the name is missing. We define the tag anonymous to mark inner classes. Table 6.4 introduces that tag.

Table 6.4. The anonymous tag
Tag anonymous .
Applies to Class.
Type Boolean.
graphics/06tab02.jpg
Motivation and purpose Mark Java anonymous inner classes, to denote that they do not have a name.
Explanation of effect The tag is used to describe that a class is anonymous. Normally each class in a class diagram must have an explicit name. This tag allows an exception to that rule. If desired, a class marked with the anonymous tag can still have a name given in the diagram.
Expansion Does not apply.
Discussion If concrete object structures are to be denoted, then it is useful to use a virtual classname in the diagram to refer to them. This virtual name can also be used in explanations.

Figure 6.10 illustrates how the Adapter design pattern accomplishes the match between the methods testAddZeroZero() and runTest(). The use of anonymous inner subclasses allows the repeated definition of test case methods within one class.

Figure 6.10. Applying the Adapter design pattern to the runTest() method
graphics/06fig10.gif

Table 6.5 provides a variant of the JUnit 1.1 recipe. The recipe 'JUnit 1.1A: How to define test cases as inner classes' uses the technique of anonymous inner subclasses to define multiple test cases within one class. The basic idea is to override the method runTest() in each inner class individually, whereas the setUp() and tearDown() methods are typically not overridden in these inner classes.

Table 6.5. Cookbook recipe for defining test cases by means of inner classes
Recipe 'JUnit 1.1A: How to define test cases as inner classes'
Intent A given piece of code needs a set of tests that should not be defined in separate regular classes.
Classes TestCase.
Steps to Apply
  1. Decide what is to be tested, and on the appropriate setting for the tests to be defined. The following questions may help.

    • Which functionality is to be tested? A method call or an interaction between several related components?

    • What are the important test data (fixtures) to operate on?

    • What is the expected outcome?

  2. Is there similar code in the test cases that can be reused? In particular, take a look at the setUp(), runTest(), and tearDown() methods.

  3. Build a new explicit subclass, either of TestCase or of a previously defined subclass.

  4. Define/add the attributes necessary to hold the object structures (fixtures) needed for the test.

  5. Optional: override setUp(). The default implementation in TestCase is empty. If subclassing another test, setUp() may adapt the result of a super.setUp() call or just reuse the inherited method. If changing an existing test class, do not override setUp() directly, but in the anonymous subclass only. By analogy with the runTest() adaptation, you can provide a new method that can be adapted to setUp().

  6. Define appropriate testing methods which must be invoked by runTest() in the inner subclasses.

  7. Optional: override tearDown() to clean up the system after the test. The same considerations/restrictions apply as for method setUp().

Discussion
  • See also the discussion and explanation of the recipe 'JUnit 1.1: How to define a test case'.

  • Adding new tests to an existing test class is suitable if the test class follows the policy of defining tests through inner subclasses already. Tests grouped in one test class should not only test related functionality, but also be structured similarly.

  • Due to the enforcement of strict test separation, setUp() is called for each test individually.This allows the tests to modify the fixture each time, but leads to time consuming overheads if many tests are defined in the same suite that operate on many different parts of the fixture defined by setUp(). Then it is useful to define a part of the used fixture in setUp(), and a less common part individually by test methods.

Although the above recipe is still straightforward, it illustrates that several aspects have to be considered. In particular, the existence of adaptation paths different from the standard pathway increase the length and content of a recipe considerably. If a recipe becomes too complex, it is useful to extract parts into subrecipes. In particular, a presentation of unlikely alternatives can then be discussed in separate recipes without cluttering the main recipe too much.

Example 6.6 shows parts of the code that results from an application of the above recipe to the ComplexNumber example. (The missing part the inner classes are presented in Section 6.3 where test suites are considered). As the construction of complex numbers is cheap, two objects are instantiated in the setUp() code. These complex numbers are used in several tests. Additional objects are created in the individual tests on demand. Figure 6.11 shows the UML-F class diagram that illustrates this adaptation.

Figure 6.11. Multiple overriding of the runTest() method
graphics/06fig11.gif
Example 6.6 Some test case definitions for the ComplexNumber class
 public class ComplexTest extends TestCase {      private ComplexNumber fZeroZero;      private ComplexNumber fZeroOne;      public ComplexTest (String name) {               super(name);      }      protected void setUp() {           fZeroZero = new ComplexNumber(0, 0);           fZeroOne = new ComplexNumber(0, 1);      }      public void testAddZeroZero() {           ComplexNumber num = new ComplexNumber(3, 7);           ComplexNumber result = num.add(fZeroZero);           assert(num.equals(result));      }      public void testAddCommuting() {           ComplexNumber num1 = new ComplexNumber(25, 9);           ComplexNumber num2 = new ComplexNumber(17, 45);           ComplexNumber result1 = num1.add(num2);           ComplexNumber result2 = num2.add(num1);           assert(result1.equals(result2));      }      public void testEquals() {           /* assert is provided by JUnit in the Assert           class, which is the super class of TestCase */           assert(!fZeroOne.equals(fZeroZero));      }      // add more tests here } 


The UML Profile for Framework Architectures
The UML Profile for Framework Architectures
ISBN: 0201675188
EAN: 2147483647
Year: 2000
Pages: 84

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