INTRODUCTION TO PROGRAMMING IN JAVA: CREATING A TEST HARNESS (PLUS NOTE ON METHOD CALLS)

NOTE: This set of www pages is not the set of www pages for the curent version of COMP101. The pages are from a previous version that, at the request of students, I have kept on line.


CONTENTS

1. Overview
2. Method calls and the return keyword
3. Argument passing
4. Example problem - Pythagoras
4.1. Requirements
 
4.2. Analysis
4.3. Design
4.4. Implementation
4.5. Testing
5. Creating a "test harness"

Example introduces: (1) the use of the pow and sqrt class methods contained in the Math class, and (2) a further (to that given in Section 1) illustration of how to return values from methods using the keyword return. Note also that the Triangle class will be reused in the following lecture.




1. OVERVIEW

When developing a new class it makes software engineering sense to test the class as thoroughly as possible before using it on a real application. We have already looked at two testing techniques arithmetic testing and data validation. Another technique is to build a test harness to automate the arithmetic testing process. This is a specially written Java class (the same as any other) designed to "exercise" a newly developed class.

We shall be illustrating such a test harness in Section 5, which will be used to test the Java code produced in the example problem presented in Section 4. Before we look at the example problem and the test harness we will (Sections 2 and 3) briefly reconsider method calls, the process of returning a value after a method call and argument passing.




2. METHOD CALLS AND THE return KEYWORD


The process of calling one method from within another method (as illustrated previously) is referred to as a method call (in imperative language the process is known as routine invocation). The method call names the method and supplies the arguments (if any) to be supplied to the method. As the result of a method call "control" is passed from the calling method to the target method. On completion control is returned to the point immediately after the call (Figure 1).

FLOW OF CONTROL WITH RESPECT TO ROUTINE INVOCATION

Figure 1: Flow of control with respect to method calls

 

If the method we are calling is defined in some other class then we must call it by either:

  1. Linking it to the class name if it is a class method.
  2. Linking it to an instance of the class if it is an instance method.

Consider the code fragment given in Table 1. Here we define a class with a private instance variable and a public instance method which returns the instance variable multiplied by 3.

The code given in Table 2 presents an application class that makes use of the class given in Table 1. Two instances of the class ExampleClass, instance1 and instance2 are created; each of which calls the treble method in turn. For the first instance the return value is assigned to a local variable (result1), for the second instance the call to the treble method has been embedded in the output statement.

The resulting output will be as follows:

Instance 1 = 6
Instance 2 = 9
// Example Class Version 1
// Frans Coenen
// 25 July 2000
// Dept Computer Science, University of Liverpool

class ExampleClassVer1 {
    
    // ------------------- FIELDS ------------------------ 
    
    private int value; 
    
    // -----------------CONSTRUCTORS --------------------- 
    
    /* Constructor */
    
    public ExampleClassVer1(int num) {
        value = num;
	}
	
    // ------------------ METHODS ------------------------ 
    
    public int treble() {
        return(value*3);
	}
    }

Table 1: Example class (version 1)

// Application Class
// Frans Coenen
// 25 July 2000
// Dept Computer Science, University of Liverpool

class ApplicationClass {
    
    // ------------------- FIELDS ------------------------ 
    
    /* None */
    
    // ------------------ METHODS ------------------------ 
    
    /* Main method */
    
    public static void main(String[] args) {
        
	// Create two instances of the Example class
	
	ExampleClassVer1 instance1 = new ExampleClassVer1(2);
	ExampleClassVer1 instance2 = new ExampleClassVer1(3);
	
	// Process
	
	int result1 = instance1.treble();
	System.out.println("Instance 1 = " + result1);
	System.out.println("Instance 2 = " + instance2.treble());
	}
    }

Table 2: Application class with method calls




3. METHOD CALLS AND ARGUMENT PASSING


In the examples given so far we have included arguments in the main methods (the yet to be explained String[] args) and constructors, both can be considered to be special kinds of method. Although we have noted that arguments can be passed to "ordinary" methods as well, we have not yet looked at example of this --- we will do this in this section. (note that the main method is a bit different in that it must have an argument, in all other cases arguments are optional).

We pass arguments to ordinary instance and class methods in the same way that we have passed arguments to constructors. Methods, including constructors (but not main), can have any number of arguments (main has one). We have seen that when defining a method we include the arguments (if any) in the signature of the method. The argumenets are referred to as the formal parameters of the method.

An example is given in Table 3 where a revised version of the code presented in Table 1 is given. Note that in this case the value "field" is passed as an argument to the treble method, in which case value is no longer a field but a simple data item that happens to be a formal parameter.

 

Thus, in the example, we have no fields and therefore we do not need a specific constructor to set the value of the field --- instead we use the default constructor which is created automatically on compilation.

In Table 4 we present an application class to be used in conjunction with the code presented in Table 3. Note that in this case we create only one instance of the class ExampleClassVer2 which we can use to call the instance method treble any number of times. When we pass a value to the treble method, for example 2, we say that the value 2 becomes the actual parameter to the method. So on the first call the value 2 is the actual parameter, and on the second call the value 3 is the actual parameter. If we have more than one argument the actual parameters are matched up to the formal parameters in the order that they are listed in the signature of the method.

That advantage offered by the argument passing mechanism is that the same method can be used to perfoma some operation any number of times but with different input data.

// Example Class
// Frans Coenen
// 14 July 2003
// Dept Computer Science, University of Liverpool

class ExampleClassVer2 {
    
    // ------------------- FIELDS ------------------------ 
    
    /* None */
    
    // -----------------CONSTRUCTORS --------------------- 
    
    /* Default constructor only */
	
    // ------------------ METHODS ------------------------ 
    
    public int treble(int value) {
        return(value*3);
	}
    }

Table 3: Example class (version 2)

// Application Class
// Frans Coenen
// 25 July 2000
// Dept Computer Science, University of Liverpool

class ApplicationClass {
    
    // ------------------- FIELDS ------------------------ 
    
    /* None */
    
    // ------------------ METHODS ------------------------ 
    
    /* Main method */
    
    public static void main(String[] args) {
        
	// Create an instance of the Example class
	
	ExampleClassVer2 instance = new ExampleClassVer2();
	
	// Process
	
	int result1 = instance.treble(2);
	System.out.println("Instance 1 = " + result1);
	System.out.println("Instance 2 = " + instance.treble(3));
	}
    }

Table 4: Application class with method calls and argument passing

Note: the examples given in Tables 1 to 4 are intended purely to illustrate the concepts of passing arguments to and from methods --- as such the examples are somewhat contrived.



4. EXAMPLE PROBLEM - PYTHAGORAS


4.1. Requirements

Given the length of the "opposite" (O) and "adjacent" (A) sides of a right angled triangle (see Figure 2) determine the "hypotenuse" (H) using Pythagoras' theorem:

H^2 = O^2 + A^2

Assume sides are to be input as double precision floating point numbers.

TRIANGLE

Figure 2: Triangle geometry

PYTHAGORAS CLASS DIAGRAM

Figure 3: Pythagoras class diagram


4.2 Analysis

A class diagram for the proposed solution is presented in Figure 3.


4.3 Design

From the above (Figure 3) the design comprises two classes, Triangle and PythagorasApp.


4.3.1 Triangle Class

Field Summary
private double opposite
           An instance field to store the opposite side of a triangle (instance of the class Triangle).
private double adjacent
           An instance field to store the adjacent side of a triangle (instance of the class Triangle).
private double hypotenuse
           An instance field to store the hypotenuse of a triangle (instance of the class Triangle).

Constructor Summary
Triangle(double newOpp, double newAdj)
           Constructs an instance of the class Triangle given opposite and adjacent sides as formal parameters which are assigned to the instance variable opposite and adjacent.

Method Summary
public void calclulateHypotenuse()
           Instance method to calculate third side of triangle, assign the result to the instance field hypotenuse. The method operates using calls to the pow and sqrt Math class methods.
public double getHypotenuse()
           Instance method to return the value of the hypotenuse field.

Nassi-Shneiderman charts for the above methods and constructor are presented in Figure 4. Note the nesting of the routine invocations.

NASSI-SHNEIDERMAN CHARTS FOR TRIANGLE CLASS METHODS

Fig 4: Nassi-Shneiderman charts for Triangle class methods


4.3.2 PythagorasApp Class

Field Summary
private static Scanner input
           A class instance to facilitate input from the input stream.

Method Summary
public static void main(String[] args)
           Main method. Creates a new instance of the class triangle using values for adjacent and opposite sides input by the user, and then invokes the calculateHypotenuse method to determine the third side which is then output using getHypotenuse.

A Nassi-Shneiderman for the above is presented in Figure 5.

NASSI-SHNEIDERNAN CHART FOR PYTHAGORAS APP METHOD

Fig 5: Nassi-Shneiderman charts for PythagorasApp class method


4.4. Implementation


4.4.1 Triangle class

// TRIANGLE
// Frans Coenen
// Wednesday 3 March 1999
// University of Liverpool

class Triangle {

    // ---------------------- FIELDS ---------------------

    private double opposite, adjacent, hypotenuse;

    // ------------------ CONSTRUCTORS -------------------

    /* Constructor */

    public Triangle(double newOpp, double newAdj) {
        opposite = newOpp;
        adjacent = newAdj;
        }

    // --------------------- METHODS ---------------------

    /* Calculate hypotenuse */

    public void calculateHypotenuse() {

        // Calculate hypotenuse

        hypotenuse = Math.sqrt(Math.pow(opposite,2)+Math.pow(adjacent,2));
        }

    /* Return Hypotenuse */

    public double getHypotenuse() {
        return(hypotenuse);
        }
    }   

Table 5: Triangle Class definition


4.4.1 PythagorasApp class

// PYTHAGORAS APPLICATION CLASS
// Frans Coenen
// Wednesday 3 March 1999
// Revised: Friday 13 may 2005
// University of Liverpool

import java.util.*;

class PythagorasApp {

    // ------------------- FIELDS ------------------------               
    
    // Create Scanner class instance
    
    private static Scanner input = new Scanner(System.in);
    
    // ------------------ METHODS ------------------------  
    
    /** Main method */
    
    public static void main(String argv[]) {
    	Triangle newTriangle;
    	double oppositeSide, adjacentSide, hypotenuseSide;
	
 	// Input values for opposite and adjavent sides
	
	System.out.println("Input opposite and adjacent sides of a " +
		"right angled triangle");
	oppositeSide = input.nextDouble();
	adjacentSide = input.nextDouble();
	
	// Call constructor with input
	
	newTriangle = new Triangle(oppositeSide,adjacentSide);

	// Calculate hypotenuse
	
	newTriangle.calculateHypotenuse();
	
	// Output result 
	
	System.out.println("Given:  opposite = " + oppositeSide +
		", adjacent = " + adjacentSide);
	hypotenuseSide = newTriangle.getHypotenuse();
	System.out.println("Result: hypyenuse = " + hypotenuseSide);	
        }
    }    

Table 6: Pythagoras application program


4.5. Testing

Arithmetic testing: We have two inputs which can be negative, zero or positive. Thus we should test all 9 (3x3) possible combinations. A suitable set of test cases is presented in the table to the right. Clearly, although it makes no difference to the end result, we should include some mechanism where by we can prevent the user from including negative results, however with our present knowledge this is not possible. What is important here is to ensure that the program operates as expected (i.e. it produces the expected result for each test case and does not cause a "crash").

TEST CASEEXPECTED RESULT
oppositeadjacenthypotenuse
4.03.05.0
4.00.04.0
4.0-3.05.0
0.03.03.0
0.00.00.0
0.0-3.03.0
-4.03.05.0
-4.00.04.0
-4.0-3.05.0

Data validation testing: We should also include some tests using input data which is deliberately of the wrong type or to many inputs, no inputs etc.

NOTE: We will be reusing the Triangle class again!




5. CREATING A "TEST HARNESS"

From the above 9 test cases are required to test the Triangle class. We can reduce the testing effort by writing a test harness (or "driver program"). The aim is to provide methods, in the test harness, to exercise each of the methods in our newly developed class. Some suitable Java code to do this is given in Table 7. The code comprises three methods, a main method and two others: includeAdjValue and makeCalculationAndOutput.

The main method includes three calls to includeAdjValue, with a different value for the formal parameter on each occasion. The includeAdjValue method then calls makeCalculationAndOutput three times, with the received argument, but alos with a different second argument on each occasion. The makeCalculationAndOutput method is therefore called 3x3 times in total, each time with different parameters. A schematic, illustrating the "flow of control" through the software, is given in Figure 6.

 

Note that the methods included in the test harness given in Table 7 are all class (static) methods which can be called directly from within a class. Hopefully the results produced by the test harness will be comparable with the expected test results given in the test case table. Table 8 gives the output produced by this test harness.

Given:  opposite = 4.0, adjacent = 3.0
Result: hypotenuse = 5.0
Given:  opposite = 4.0, adjacent = 0.0
Result: hypotenuse = 4.0
Given:  opposite = 4.0, adjacent = -3.0
Result: hypotenuse = 5.0
Given:  opposite = 0.0, adjacent = 3.0
Result: hypotenuse = 3.0
Given:  opposite = 0.0, adjacent = 0.0
Result: hypotenuse = 0.0
Given:  opposite = 0.0, adjacent = -3.0
Result: hypotenuse = 3.0
Given:  opposite = -4.0, adjacent = 3.0
Result: hypotenuse = 5.0
Given:  opposite = -4.0, adjacent = 0.0
Result: hypotenuse = 4.0
Given:  opposite = -4.0, adjacent = -3.0
Result: hypotenuse = 5.0

Table 8: Output from test harness


// PYTHAGORAS TEST CLASS
// Frans Coenen
// Friday 5 March 1999
// University of Liverpool

import java.io.*;

class PythagorasTest {

    // ------------------- FIELDS ------------------------
    
    /* None */    

    // ------------------ METHODS ------------------------

    /* Main method */

    public static void main(String[] args) {
        includeAdjValue(4.0);
        includeAdjValue(0.0);
        includeAdjValue(-4.0);
        }

    /* Add adjacent value */

    public static void includeAdjValue(double opposite) {
        makeCalculationAndOutput(opposite,3.0);
        makeCalculationAndOutput(opposite,0.0);
        makeCalculationAndOutput(opposite,-3.0);
        }

    /* Make calculation and output */

    public static void makeCalculationAndOutput(double oppositeSide,
                double adjacentSide) {

        // Call constructor with input

        Triangle newTriangle = new Triangle(oppositeSide,adjacentSide);

        // Calculate hypotenuse 
	
        newTriangle.calculateHypotenuse();
        
	// Output result

        System.out.println("Given:  opposite = " + oppositeSide + 
	            ", adjacent = " + adjacentSide);
        double hypotenuseSide = newTriangle.getHypotenuse();
        System.out.println("Result: hypotenuse = " + hypotenuseSide); 
	}     
    }     

Table 7: "Test harness" code

Note that when choosing a name for a method the designer should be influenced by what the method does and not what the calling method(s) might do.

Figure 6 shows an Activity Diagram for the application. It shows how "control" flows through the program. We start at the top at what is knowm as the "riser" and end at the bottom at what is known as the "sink" (the analogy is that of water flowing through pipes). The boxes with rounded corners indicate activities (method calls and calculations in this case), and the red arrows (directed arcs) the flow of control (water!). The dotted boxes in the diagram indicate methods which encompass a number of activities.

Thus we commence at the riser (the main method) and call the adajacentValue method with the argument 4.0. This then becomes the actual parameter for this method and flow of control passes from main to adajacentValue. In the adajacentValue method the makeCalc method is called three times in sequence. On the first occasion the actual parameter given to adajacentValue (4.0) and the value 3.0 are passed as arguments to the makeCalc method. Control is temporarily passed to the makeCalc method which does the necessary calculation, outputs the result (5.0) and then passes control back to the calling method. On the second occasion the adajacentValue method's actual parameter 4.0 and the value 0.0 are passed as arguments to the makeCalc method and the calculation is repeated. On the third occasion the actual parameter 4.0 and the value -3.0 are passed to makeCalc. Once makeCalc has completed its processing flow of control returns to main where the process is repeated but this time the formal parameter for the adajacentValue is passed the actual parameter 0.0. And so on.

TEST HARNESS CFD

Fig 6: Activity diagram showing the "flow of control" through the test harness program given in Table 3




Created and maintained by Frans Coenen. Last updated 10 February 2015