The UML Profile for Framework Architectures
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
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'.
Recipe 'JUnit 1: How to create automated tests in JUnit' | |
Intent | To define test cases for Java applications. |
Classes | TestCase and TestSuite. |
Related recipes |
|
Steps to Apply |
|
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:
-
Two equal numbers, represented by distinct objects;
-
Two different numbers with a non-zero real part only;
-
Two different numbers with a non-zero imaginary part only;
-
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.
Recipe 'JUnit 1.1: How to define a test case' | |
---|---|
Intent | To create a test case. |
Classes | TestCase. |
Steps to Apply |
|
Discussion |
|
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
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
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.
Tag | anonymous . |
---|---|
Applies to | Class. |
Type | Boolean. |
| |
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
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.
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 |
|
Discussion |
|
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
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 }