# Black-box Testing

In selecting our test cases for good coverage, we might want to consider both the specification and the implementation of the program or module being tested. It turns out that we can often do a pretty good job of picking test cases by just looking at the specification and ignoring the implementation. This is known as black-box testing. The idea is that we think of the code as a black box about which all we can see is its surface: its specification. We pick test cases by looking at how the specification implicitly introduces boundaries that divide the space of possible inputs into different regions.

When writing black-box test cases, we ask ourselves what set of test cases that will produce distinctive behavior as predicted by the specification. It is important to try out both typical inputs and inputs that are boundary cases aka corner cases or edge cases. A common error is to only test typical inputs, with the result that the program usually works but fails in less frequent situations. It's also important to identify ways in which the specification creates classes of inputs that should elicit similar behavior from the function, and to test on those paths through the specification. Here are some examples.

## Example 1

Here are some ideas for how to test the create function:

• Looking at the square above, we see that it has boundaries at min_int and max_int. We want to try to construct rationals at the corners and along the sides of the square, e.g. create min_int min_int, create max_int 2, etc.

• The line p=0 is important because p/q is zero all along it. We should try (0,q) for various values of q.

• We should try some typical (p,q) pairs in all four quadrants of the space.

• We should try both (p,q) pairs in which q divides evenly into p, and pairs in which q does not divide into p.

• Pairs of the form (1,q),(-1,q),(p,1),(p,-1) for various p and q also may be interesting given the properties of rational numbers.

The specification also says that the code will check that q is not zero. We should construct some test cases to ensure this checking is done as advertised. Trying (1,0), (maxint,0), (minint,0), (-1,0), (0,0) to see that they all raise the specified exception would probably be an adequate set of black-box tests.

## Example 2

Consider the function list_max:

(* Return the maximum element in the list. *)
val list_max: int list -> int


What is a good set of black-box test cases? Here the input space is the set of all possible lists of ints. We need to try some typical inputs and also consider boundary cases. Based on this spec, boundary cases include the following:

• A list containing one element. In fact, an empty list is probably the first boundary case we think of. Looking at the spec above, we realize that it doesn't specify what happens in the case of an empty list. Thus, thinking about boundary cases is also useful in identifying errors in the specification.

• A list containing two elements.

• A list in which the maximum is the first element. Or the last element. Or somewhere in the middle of the list.

• A list in which every element is equal.

• A list in which the elements are arranged in ascending sorted order, and one in which they are arranged in descending sorted order.

• A list in which the maximum element is max_int, and a list in which the maximum element is min_int.

## Example 3

Consider the function sqrt:

(* [sqrt x n] is the square root of [x] computed to an accuracy of [n]
* significant digits.
* requires: [x >= 0] and [n >= 1] *)
val sqrt : float -> int -> float


The precondition identifies two possibilities for x (either it is zero or greater) and two possibilities for n (either it is one or greater). That leads to four "paths through the specification", i.e., representative and boundary cases for satisfying the precondition, which we should test:

• x is zero and n is 1

• x is greater than zero and n is 1

• x is zero and n is greater than 1

• x is greater than zero and n is greater than 1.