Guidelines:
|
publicIsClear |
technicianClear |
Code as written... |
Correct code would have... |
|
---|---|---|---|---|
true |
true |
detonates |
detonated |
test is useless (for this fault) |
true |
false |
detonates |
not detonated |
useful test |
false |
true |
detonates |
not detonated |
useful test |
false |
false |
does not detonate |
not detonated |
test is useless (for this fault) |
The two middle tests are both useful for finding this particular fault. Note, however, that they're redundant: since either will find the fault, you needn't run both.
There are other ways in which the expression might be wrong. Here are two lists of common mistakes in boolean expressions. The faults on the left are all caught by the technique discussed here. The faults on the right might not be. So this technique doesn't catch all the faults we might like, but it's still useful.
Faults detected |
Faults possibly not detected |
---|---|
Using wrong operator: a || b should be a&&b | Wrong variable used: a&&b&&c should be a&& x&&d |
Negation is omitted or incorrect: a||b should be !a||b, or ! a||b should be a||b | Expression is too simple: a&&b should be a&&b&&c |
The expression is misparenthesized: a&&b||c should be a&&(b||c) | Expressions with more than one of the faults in the left column |
The expression is overly complex: a&&b&&c
should be a&&b (This fault is not so likely, but is easy to find with tests useful for other reasons.) |
How are these ideas used? Suppose you're given a boolean expression like a&&!b. You could construct a truth table like this one:
a |
b |
a&&!b |
maybe it should be |
maybe it should be |
maybe it should be |
... |
---|---|---|---|---|---|---|
true |
true |
false |
true |
false |
true |
... |
true |
false |
true |
true |
false |
false |
... |
false |
true |
false |
false |
false |
false |
... |
false |
false |
false |
true |
true |
false |
... |
If you crunched through all the possibilities, you'd find that the first, second, and fourth possibilities are all that's needed. The third expression will find no faults that won't be found by one of the others, so you needn't try it. (As the expressions grow more complicated, the savings due to unneeded cases grow quickly.)
Of course, no one sane would build such a table. Fortunately, you don't have to. It's easy to memorize the required cases for simple expressions. (See the next section.) For more complex expressions, such as A&&B||C, see Test Ideas for Mixtures of ANDs and ORs, which lists test ideas for expressions with two or three operators. For even more complex expressions, a program can be used to generate test ideas.
If the expression is A&&B, test with:
A |
B |
---|---|
true |
true |
true |
false |
false |
true |
If the expression is A||B, test with:
A |
B |
---|---|
true |
false |
false |
true |
false |
false |
If the expression is A1 && A2 && ... && An, test with:
A1, A2, ..., and An are all true |
A1 is false, all the rest are true |
A2 is false, all the rest are true |
... |
An is false, all the rest are true |
If the expression is A1 || A2 || ... || An, test with:
A1, A2, ..., and An are all false |
A1 is true, all the rest are false |
A2 is true, all the rest are false |
... |
An is true, all the rest are false |
If the expression is A, test with:
A |
true |
false |
So, when you need to test a&&!b, you can apply the first table above, invert the sense of b (because it's negated), and get this list of Test Ideas:
Here's another example of code with a fault:
if (finished < required) { siren.sound(); }
The < should be a <=. Such mistakes are fairly common. As with boolean expressions, you can construct a table of test values and see which ones detect the fault:
finished |
required |
code as written... |
the correct code would have... |
---|---|---|---|
1 |
5 |
sounds the siren |
sounded the siren |
5 |
5 |
does not sound the siren |
sounded the siren |
5 |
1 |
does not sound the siren |
not sounded the siren |
More generally, the fault can be detected whenever finished=required. From analyses of plausible faults, we can get these rules for test ideas:
If the expression is A<B or A>=B, test with
A=B |
A slightly less than B |
If the expression is A>B or A<=B, test with
A=B |
A slightly larger than B |
What does "slightly" mean? If A and B are integers, A should be one less than or larger than B. If they are floating point numbers, A should be a number quite close to B. (It's probably not necessary that it be the the closest floating point number to B.)
Most relational operators occur within boolean expressions, as in this example:
if (finished < required) { siren.sound(); }
The rules for relational expressions would lead to these test ideas:
The rules for boolean expressions would lead to these:
But if finished is slightly less than required, finished < required is true, so there's no point in writing down the latter.
And if finished equals required, finished < required is false, so there's no point in writing down that latter one either.
So, if a relational expression contains no boolean operators (&& and ||), ignore the fact that it's also a boolean expression.
Things are a bit more complicated with combinations of boolean and relational operators, like this one:
if (count<5 || always) { siren.sound(); }
From the relational expression, you get:
From the boolean expression, you get:
These can be combined into three more specific test ideas. (Here, note that count is an integer.)
Notice that count=5 is used twice. It might seem better to use it only once, to allow the use of some other value-after all, why test count with 5 twice? Wouldn't it be better to try it once with 5 and another time with some other value such that count<5 is false? It would be, but it's dangerous to try. That's because it's easy to make a mistake. Suppose you tried the following:
Suppose that there's a fault that can only be caught with count=5. What that means is that the value 5 will cause count<5 to produce false in the second test, when the correct code would have produced true. However, that false value is immediately or'd with the value of always, which is true. That means the value of the whole expression is correct, even though the value of the relational subexpression was wrong. The fault will go undiscovered.
The fault doesn't go undiscovered if it's the other count=5 that is left less specific.
Similar problems happen when the relational expression is on the right-hand side of the boolean operator.
Because it's hard to know which subexpressions have to be exact and which can be general, it's best to make them all exact. The alternative is to use the boolean expression program mentioned above. It produces correct test ideas for arbitrary mixed boolean-and-relational expressions.
As explained in Concepts: Test-first Design, it's usually preferable to design tests before implementing code. So, although the techniques are motivated by code examples, they'll usually be applied without code. How?
Certain design artifacts, such as statecharts and sequence diagrams, use boolean expressions as guards. Those cases are straightforward-simply add the test ideas from the boolean expressions to the artifact's test idea checklist. See Guidelines: Test Ideas for Statechart and Activity Diagrams.
The trickier case is when boolean expressions are implicit rather than explicit. That's often the case in descriptions of APIs. Here's an example. Consider this method:
List matchList(Directory d1, Directory d1, FilenameFilter excluder);
The description of this method's behavior might read like this:
Returns a List of the absolute pathnames of all files that appear in both Directories. Subdirectories are descended. [...] Filenames that match the excluder are excluded from the returned list. The excluder only applies to the top-level directories, not to filenames in subdirectories.
The words "and" and "or" do not appear. But when is a filename included in the return list? When it appears in the first directory and it appears in the second directory and it's either in a lower level directory or it's not specifically excluded. In code:
if (appearsInFirst && appearsInSecond && (inLowerLevel || !excluded)) { add to list }
Here are the test ideas for that expression, given in tabular form:
appearsInFirst |
appearsInSecond |
inLower |
excluded |
---|---|---|---|
true |
true |
false |
true |
true |
true |
false |
false |
true |
true |
true |
true |
true |
false |
false |
false |
false |
true |
false |
false |
The general approach for discovering implicit boolean expressions from text
is to first list the actions described (such as "returns a matching name").
Then write a boolean expression that describes the cases in which an action
is taken. Derive test ideas from all the expressions.
There's room for disagreement in that process. For example, one person might write down the boolean expression used above. Another might say that there are really two distinct actions: first, the program discovers matching names, then it filters them out. So, instead of one expression, there are two:
These different approaches can lead to different test ideas and thus different tests. But the differences are most likely not particularly important. That is, the time spent worrying about which expression is right, and trying alternatives, would be better spent on other techniques and producing more tests. If you're curious about what the sorts of differences might be, read on.
The second person would get two sets of test ideas.
test ideas about discovering a match:
- file in first directory, file in second directory (true, true)
- file in first directory, file not in second directory (true, false)
- file not in first directory, file in second directory (false, true)
test ideas about filtering a match (once one has been discovered):
- matching files are in the top level, the name matches the excluder (true, true)
- matching files are in the top level, the name doesn't match the excluder (true, false)
- matching files are in some lower level, the name matches the excluder (false, true)
Suppose those two sets of test ideas are combined. The ones in the second set only matter when the file is in both directories, so they can only be combined with the first idea in the first set. That gives us the following:
file in first directory |
file in second directory |
in top level |
matches excluder |
---|---|---|---|
true |
true |
true |
true |
true |
true |
true |
false |
true |
true |
false |
true |
Two of the test ideas about discovering a match do not appear in that table. We can add them like this:
file in first directory |
file in second directory |
in top level |
matches excluder |
---|---|---|---|
true |
true |
true |
true |
true |
true |
true |
false |
true |
true |
false |
true |
true |
false |
- |
- |
false |
true |
- |
- |
The blank cells indicate that the columns are irrelevant.
This table now looks rather similar to the first person's table. The similarity can be emphasized by using the same terminology. The first person's table has a column called "inLower", and the second person's has one called "in top level". They can be converted by flipping the sense of the values. Doing that, we get this version of the second table:
appearsInFirst |
appearsInSecond |
inLower |
excluded |
---|---|---|---|
true |
true |
false |
true |
true |
true |
false |
false |
true |
true |
true |
true |
true |
false |
- |
- |
false |
true |
- |
- |
The first three rows are identical to the first person's table. The last two differ only in that this version doesn't specify values that the first does. This amounts to an assumption about the way the code was written. The first assumed a complicated boolean expression:
if (appearsInFirst && appearsInSecond && (inLowerLevel || !excluded)) { add to list }
The second assumes nested boolean expressions:
if (appearsInFirst && appearsInSecond) { // found match. if (inTopLevel && excluded) { // filter it } }
The difference between the two is that the test ideas for the first detect two faults that the ideas for the second do not, because those faults don't apply.
Rational Unified Process |