Wednesday, November 24, 2010

White Box Testing

White box testing is very different in nature from black box testing. In black box testing, focus of all the activities is only on the functionality of system and not on what is happening inside the system.

Purpose of white box testing is to make sure that
  • Functionality is proper
  • Information on the code coverage
White box is primarily development teams job, but now test engineers have also started helping development team in this effort by contributing in writing unit test cases, generating data for unit test cases etc.


White box testing can be performed at various level starting from unit to system testing. Only distinction between black box and white box is the system knowledge, in white box testing you execute or select your test cases based on the code/architectural knowledge of system under test.

Even if you are executing integration or system test, but using data in such a way that one particular code path is exercised, it should fall under white box testing.

There are different type of coverage that can be targeted in white box testing
  • Statement coverage
  • Function coverage
  • Decision coverage
  • Decision and Statement coverage
Conditional Testing

The first improvement to white box techniques is to ensure that the Boolean controlling expressions are adequately tested, a process known as condition testing.

The process of condition testing ensures that a controlling expression has been adequately exercised whilst the software is under test by constructing a constraint set for every expression and then ensuring that every member on the constraint set is included in the values which are presented to the expression. This may require additional test runs to be included in the test plan.

To introduce the concept of constraint sets the simplest possible Boolean condition, a single Boolean variable or a negated Boolean variable, will be considered. These conditions may take forms such as:

if DateValid then while not DateValid then

The constraint set for both of these expressions is {t ,f } which indicates that to adequately test these expressions they should be tested twice with DateValid having the values True and False.

Perhaps the next simplest Boolean condition consists of a simple relational expression of the form value operator value, where the operator can be one of: is equal to ( = ), is not equal to ( /= ), is greater than ( > ), is less than ( = ) and is less than or equal to ( , <}, which indicates that to adequately test a relational expression it must be tested three times with values which ensure that the two values are equal, that the first value is less than the second value and that the first value is greater than the second value.

More complex control expressions involve the use of the Boolean operators, and, or and xor, which combine the values of two Boolean values. To construct a constraint set for a simple Boolean expression of the form BooleanValue operator BooleanValue all possible combinations of True and False have to be considered. This gives the constraint set for the expression as {{t ,t } {t ,f } {f ,t } {f ,f }}. If both BooleanValues are simple or negated Boolean variables then no further development of this set is required. However if one or both of the BooleanValues are relational expressions then the constraint set for the relational expression will have to be combined with this constraint set. The combination takes the form of noting that the equality condition is equivalent to true and the both inequality conditions are equivalent to false. Thus every true condition in the condition set is replaced with ' = ' and every false replaced twice, once with ' > ' and once with ' < '.

Thus if only one the left hand BooleanValue is a relational expression the condition set would be {{= ,t } {= ,f } {> ,t } { ,f } { } {= , ,= } { ,> } {> ,< } { } {< ,< }}.

An increase in the complexity of the Boolean expression by the addition of more operators will introduce implicit or explicit bracketing of the order of evaluation which will be reflected in the condition set and will increase the number of terms in the set. For example a Boolean expression of the following form:

BooleanValue1 operator1 BooleanValue2 operator3nbsp;BooleanValue3

Has the implicit bracketing:

(BooleanValue1 operator1 BooleanValue2) operato3 BooleanValue3

The constraint set for the complete expression would be {{e1,t}{e1,f}}, where e1 is the condition set of the bracketed sub-expression and when it is used to expand this constraint set gives {{t,t,t} {t,f,t} {f,t,t} {f,f,t} {t,t,f} {t,f,f} {f,t,f} {f,f,f}}. If any of the BooleanValues are themselves relational expressions this will increase the number of terms in the condition set. In this example the worst case would be if all three values were relational expressions and would produce a total of 27 terms in the condition set. This would imply that 27 tests are required to adequately test the expression. As the number of Boolean operators increases the number of terms in a condition set increases exponentially and comprehensive testing of the expression becomes more complicated and less likely. It is this consideration which led to the advice, initially given in Section 1, to keep Boolean control expressions as simple as possible, and one way to do this is to use Boolean variables rather than expressions within such control conditions.
Data Life Cycle Testing

Keeping control expressions simple can be argued to simply distribute the complexity from the control expressions into the other parts of the subprogram and an effective testing strategy should recognise this and account for it. One approach to this consideration is known as data lifecyle testing, and it is based upon the consideration that a variable is at some stage created, and subsequently may have its value changed or used in a controlling expression several times before being destroyed. If only locally declared Boolean variables used in control conditions are considered then an examination of the source code will indicate the place in the source code where the variable is created, places where it is given a value, where the value is used as part of a control expression and the place where it is destroyed.

This approach to testing requires all possible feasible lifecycles of the variable to be covered whilst the module is under test. In the case of a Boolean variable this should include the possibility of a Boolean variable being given the values True and False at each place where it is given a value. A typical outline sketch of a possible lifecycle of a controlling Boolean variable within a subprogram might be as follows:

~~~ SomeSubProgram( ~~~ ) is

ControlVar : BOOLEAN := FALSE;

begin -- SomeSubProgram

~~~

while not ControlVar loop

~~~

ControlVar := SomeExpression;

end loop;

~~~

end SomeSubProg;

In this sketch ~~~ indicates the parts of the subprogram which are not relevant to the lifecycle. In this example there are two places where the variable ControlVar is given a value, the location where it is created and the assignment within the loop. Additionally there is one place where it is used as a control expression. There are two possible lifecycles to consider, one which can be characterised as { f t } indicating that the variable is created with the value False and given the value True upon the first iteration of the loop. The other lifecycle can be characterised as { f, f, ... t }, which differs from the first by indicating that the variable is given the value False on the first iteration, following which there is the possibility of more iterations where it is also given the value False, being given the value True on the last iteration.

Other possible lifecycles such as { f } or { f t f .. t } can be shown from a consideration of the source code not to be possible. The first is not possible as the default value will ensure that the loop iterates causing the variable to experience at least two values during its lifecycle. The second is not possible as soon as the variable is given the value True within the loop, the loop and subsequently the subprogram will terminate, causing the variable to be destroyed.

This quick look at the variable lifecycle approach only indicates the basis of this approach to white box testing. It should also indicate that this is one of the most laborious and thus expensive and difficult techniques to apply. As it is expensive and does not add a great deal to the testing considerations which have already been discussed, it is not widely used.
Loop Testing

The final white box consideration which will be introduced is the testing of loops, which have been shown to be the most common cause of faults in subprograms. If a loop, definite or indefinite, is intended to iterate n times then the test plan should include the following seven considerations and possible faults.
  • That the loop might iterate zero times.
  • That the loop might iterate once
  • That the loop might iterate twice
  • That the loop might iterate several times
  • That the loop might iterate n - 1 times
  • That the loop might iterate n times
  • That the loop might iterate n + 1 times
  • That the loop might iterate infinite times
All feasible possibilities should be exercised whilst the software is under test. The last possibility, an infinite loop, is a very noticeable, and common fault. All loops should be constructed in such a way that it is guaranteed that they will at some stage come to an end. However this does not necessarily guarantee that they come to an end after the correct number of iterations, a loop which iterates one time too many or one time too few is probably the most common loop fault. Of these possibilities an additional iteration may hopefully cause a CONSTRAINT_ERROR exception to be raised announcing its presence. Otherwise the n - 1 and n + 1 loop faults can be very difficult to detect and correct.

A loop executing zero times may be part of the design, in which case it should be explicitly tested that it does so when required and does not do so when it is not required. Otherwise, if the loop should never execute zero times, and it does, this can also be a very subtle fault to locate. The additional considerations, once, twice and many are included to increase confidence that the loop is operating correctly.

The next consideration is the testing of nested loops. One approach to this is to combine the test considerations of the innermost loop with those of the outermost loop. As there are 7 considerations for a simple loop, this will give 49 considerations for two levels of nesting and 343 considerations for a triple nested loop, this is clearly not a feasible proposition.

What is possible is to start by testing the innermost loop, with all other loops set to iterate the minimum number of times. Once the innermost loop has been tested it should be configured so that it will iterate the minimum number of times and the next outermost loop tested. Testing of nested loops can continue in this manner, effectively testing each nested loop in sequence rather than in combination, which will result in the number of required tests increasing arithmetically ( 7, 14, 21 ..) rather than geometrically ( 7, 49, 343 .. ).

No comments:

Post a Comment