COMP2100/2500 Software Construction

Lecture 9: Software Testing (1): the role of software testing and types of testing

Summary

Topics of the lecture:


Testing is purportedly a procedure to establish whether the system meets the specification requirements, but in reality it seeks to uncover operational faults (bugs) in software. These are properly known as "defects".

The term "bug" has been used for minor faults in machinery since the 19th Century or earlier. A real "bug" (a fried moth) was found in an early computer, the Harvard Mark II machine in Sept. 1947 and noted by programming pioneer Grace Hopper—but this was certainly not the first use of the term. picture of the log page


Types of testing

Definition

Verification:
Are we building
the product right?
Validation:
Are we building the right product?

V&V (Verification and Validation) should begin at the earliest possible stages of the software development cycle:
because the cost of debugging grows exponentially with the progression through the project.

  1. Requirements

  2. Specification

  3. Planning

  4. Design

  5. Implementation

  6. Integration

  7. Maintenance

phases of the SDLC
Software Development Life Cycle

Validation techniques include the following:

Requirements Reviews:
 
Specifications reviewed by:
QA group feasible only for large organisations.
Rapid Prototyping:

Prototype components are built for client demonstration.
Such components need be neither complete, nor reliable.

Formal Specification:

A mathematical description of the software system is constructed.
Deriving and proving properties mathematically can uncover incompleteness, ambiguity, and contradictions.


Verification

verification terminology

IEEE definitions

Some authors (e.g. Bertrand Meyer) do not use these terms, but call them "fault", "defect" and "error", respectively.

A software product is correct if it will always behave as specified.
(Which doesn't mean that it is what the customer wants.)

Types of verification

Code and Design Inspection:
Code is reviewed by members of:
Formal Verification:
 
Mathematical proofs are given to show the code meets critical requirements.
Based on having formal requirements, and mathematical models which include machine arithmetics and logic of computational process (Hoare logic etc).
Requires highly qualified staff, rare; but tools are gradually becoming available, special training programs are introduced.

This is hard to do for small software, and very difficult to do for large scale software, requires great expertise.

A correctness proof was recently achieved for a version of the Unix microkernel by researchers in NICTA (National ICT Australia, ANU CECS is a partner): a world first, significant result.

 
 
 
 

 
 

Testing:
Run the software on sample inputs and check the results.

None of these alone is sufficient.

Testing is the most common,
and every programmer needs to know how to test properly
to get programs that are closer to being "correct"
in least time and less wasted effort.


Testing Phases

Unit Testing:
Individual software components (classes) are tested in isolation.
Integration Testing:
Software components are integrated and tested to check that they work together.
System Testing:
The entire product is tested as it is intended to be used.
Acceptance (α) Testing:
Testing through use by the client.

All of these can use a technique called

Regression Testing:
Rerunning old tests after a change.
This is desirable because "for every 3 faults fixed, 1 new fault is introduced!" (empirical law)

In unit testing the class is isolated from the other classes:

Consider a class X under test:

The idea of using test stubs is not unique to software. Here is a physical test stub.

Crush dummy test as a stub.

And this is how the poor stubs feel afterwards — when the test succeeds and when it fails:

Crush dummy brushing.Crush dummy is hurt.


Relative nature of software correctness

Correctness of a program is not an absolute, but relative (achieved in the conditions which are defined as operational for this program).

Testing is intended to increase our confidence in the correctness of something, in the conditions in which we intend to use it.

Testing strategies

The primary objective of testing is to make the system fail!

two quotes by famous scientists

Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.
Edsger W. Dijkstra computer scientist

Compare this with an earlier statement:

Experiment can only prove that a theory wrong, never that it's correct.
Albert Einstein physicist

Testing everything - exhaustive testing

Suppose you want to test a 64-bit floating point division routine.
There are 2128 combinations. At 1 test per nanosecond (1 billionth of a second), it'll take 1022 years!
(the age of Universe is currently estimated to be 15 billion years, ie 1.5 * 1010).

The challenge is to find a much smaller number of inputs that are likely to make the system fail,
and to know enough that we can trace back failures to the faults that cause them.

How do we choose such tests? how do we design our test cases?

pause for questions


Specification Testing:
Test cases are derived from the specification of the component.
The component is a black box: its implementation is not examined.
Structural Testing:
Test cases are derived from the implementation of the component.
The component is a white box (or "glass box"): its implementation is examined.

Used individually, specification testing is slightly more effective than structural testing.
But, it is best to use both.


Specification Testing

Test selection strategies

Equivalence Class Coverage:
Inputs can be partitioned into classes eliciting similar behaviour.
Use at least one input from each class.
Boundary Value Analysis:
Choose inputs occurring at boundaries between behaviours.
Note that implies cases with both kinds of behaviour around the boundary, not only at the single boundary of one region or the other.
Choose inputs at the extremes of possible input values.
    /** Telephone call billing information */
    public class Call {
          private int minutes; //duration of the call
  
          private int cost() 
               
          //the cost of the call in cents

  SPECIFICATION: 0 minutes costs $0;
      less than 2 minutes costs $0.20;
      up to 60 minutes costs $0.20 plus 30c per minute over 2 minutes;
      over 60 minutes is capped at $20.
  

Equivalence Classes and Boundary Values.

input value

expected output

-1

error

0

0

1

20

2

20

3

50

60

1760

61

2000



These cases cover:

  • One (or more) from every equivalence class.

  • One value on either side of each boundary.

are these statements true?

If we are testing a method to search for an element in an array, we might test using:

Equivalence Classes for a method to search an array:
Boundary Values for a method to search an array:

Structural testing

Here the test cases are derived from the implementation of the program. The component is a white box, and its implementation is examined.

Test selection strategies:

Here is the body of the implementation of the telephone billing function.

    /** Telephone call billing information */
    public class Call {
          private int minutes; //duration of the call
               
          //the cost of the call in cents

          private int cost() {

                 assert (minutes >= 0);

              if (minutes = 0) {
                    return 0;
              } else if (minutes > 0 && minutes <= 2 {
                    return 20;
              } else if (minutes  2 && minutes <= 60) {
                    return 20 + (minutes - 2) * 30;
              } else {
                        return 2000;
              }
          }
    }   
Equivalence Classes and Boundary Values.

Branch Coverage vs Path Coverage

Consider a program segment with two sequential binary choice points:
Equivalence Classes and Boundary Values.
Clearly, the path coverage is more thorough test, but:
Consider the following method:
   public void maxThree(int x, int y, int z) {
            if (x > y) {
                return x;
            } else {
                    return y;
           }
   }
In white box testing we rely on the code to infer the behaviour of the program, ie we construct our tests based on the code structure. For the above snippet, all paths which we can come up with (below) test correct, yet the code is wrong! There is a danger to rely on the code when designing the test.

Input

Expected

Actual

x=3

y=2

3

3

z=1

x=2

y=3

3

3

z=1



For loops
Branch and Path Coverage for Loops


Example of structural testing for binary search:
Structural Test for Binary Search

In the rest of this module we are going to concentrate on unit testing.

Unit testing is the closest among all kind of testing activities to code writing. This is what the programmer who writes the code should do. In fact, writing the code for unit tests is the same kind of activity. Unlike debugging, which is a process of going from knowing that the program is broken and you look for the programming error, testing is a systematic attempt to reveal bugs in a program which is thought to be working correctly. Some high level programming languages provide a mechanism to effectively check whether the behaviour of a particular unit (class, method) is the same as it should be according to the specification and the contract of this unit. This mechanism is:

Assertions.

If, say a method foo() in a class Bar should
then assertions can be used to check if the actual return value value is the same
as the required valueX (or similar test for the actual and required states of the object).
   assert(value == valueX);
If the two values (or, states) are not equivalent (the equivalence needs to be defined accordingly), the assertion violation causes the testing program to report it.

Unit Testing Framework

A particularly useful setup which makes the process of unit testing very productive is provided by the framework of classes (originally developed for Java, but now available for every language) known as

JUnit logonit Testing Framework

more on this in the next lecture
The JUnit testing framework allows the developer to define a reciprocal test class for every class in the development software. The test classes can be extended incrementally, organised in suitable collections (called suites) for testing a particular aspect of the unit behaviour. The test suites can include ever increasing number of test cases which can be devised from the specification and contracts, by analysing the equivalence classes and boundary values etc. Often, the test data (expected, or required, values) represent assets (it may be costly to obtain them, eg, using real experiments etc). The JUnit approach allows to use these assets effectively (selection of tests). JUnit also provides special Runner classes for conducting the test runs, using either an easy to use GUI, or generating test reports which you can process using additional tools.
Copyright © 2006,2007, Alexei Khorev, Chris Johnson, The Australian National University
$Revision: 1.4 $ $Date: 2009/03/24 23:28:28 $ $Author: cwj $
Feedback & Queries to comp2100@cs.anu.edu.au

Last modified: Thurs 17 March 2011