| Guidelines:
Test Ideas for Method CallsTopicsHere's an example of defective code: 
File file = new File(stringName);
file.delete();
 The defect is that File.delete can fail, but 
  the code doesn't check for that. Fixing it requires the addition of the italicized 
  code shown here: 
File file = new File(stringName);
if (file.delete() == false) {...}
 This guideline describes a method for detecting cases where your code does 
  not handle the result of calling a method. (Note that it assumes that the method 
  called produces the correct result for whatever inputs you give it. That's something 
  that should be tested, but creating test ideas for the called method is a separate 
  activity. That is, it's not your job to test File.delete.) The key notion is that you should create a test idea for each distinct unhandled 
  relevant result of a method call. To define that term, let's first look 
  at result. When a method executes, it changes the state of the world. 
  Here are some examples: 
  It might push return values on the runtime stack.It might throw an exception.It might change a global variable.It might update a record in a database.It might send data over the network.It might print a message to standard output. Now let's look at relevant, again using some examples. 
  Suppose the method being called prints a message to standard output. That 
    "changes the state of the world", but it cannot affect the further 
    processing of this program. No matter what gets printed, even nothing at all, 
    it can't affect the execution of your code.If the method returns true for success and false for failure, your program 
    very likely should branch based on the result. So that return value is relevant.If the called method updates a database record that your code later reads 
    and uses, the result (updating the record) is relevant. (There's no absolute line between relevant and irrelevant. By calling print, 
  your method might cause buffers to be allocated, and that allocation might be 
  relevant after print returns. It's conceivable 
  that a defect might depend on whether and what buffers were allocated. It's 
  conceivable, but is it at all plausible?) A method might often have a very large number of results, but only some of 
  them will be distinct. For example, consider a method that writes bytes 
  to disk. It might return a number less than zero to indicate failure; otherwise, 
  it returns the number of bytes written (which might be fewer than the number 
  requested). The large number of possibilities can be grouped into three distinct 
  results: 
  a number less than zero.the number written equals the number requestedsome bytes were written, but less than the number requested. All the values less than zero are grouped into one result because no reasonable 
  program will make a distinction among them. All of them (if, indeed, more than 
  one is possible) should be treated as an error. Similarly, if the code requested 
  that 500 bytes be written, it doesn't matter if 34 were actually written or 
  340: the same thing will probably be done with the unwritten bytes. (If something 
  different should be done for some value, such as 0, that will form a new distinct 
  result.) There's one last word in the defining term to explain. This particular testing 
  technique is not concerned with distinct results that are already handled. 
  Consider, again, this code: 
File file = new File(stringName);
if (file.delete() == false) {...}
 There are two distinct results (true and false). The code handles them. It 
  might handle them incorrectly, but test ideas from Guideline: 
  Test Ideas for Booleans and Boundaries  will check that. This test technique 
  is concerned with distinct results that are not specifically handled by distinct 
  code. That might happen for two reasons: you thought the distinction was irrelevant, 
  or you simply overlooked it. Here's an example of the first case: 
result = m.method();
switch (result) {
    case FAIL:
    case CRASH:
       ...
       break;
    case DEFER:
       ...
       break;
    default:
       ...
       break;
}
 FAIL and CRASH 
  are handled by the same code. It might be wise to check that that's really appropriate. 
  Here's an example of an overlooked distinction: 
result = s.shutdown();
if (result == PANIC) {
   ...
} else {
   // success! Shut down the reactor.
   ...
} 
 It turns out that shutdown can return an additional distinct result: RETRY. 
  The code as written treats that case the same as the success case, which is 
  almost certainly wrong. So your goal is to think of those distinct relevant results you previously 
  overlooked. That seems impossible: why would you realize they're relevant now 
  if you didn't earlier? The answer is that a systematic re-examination of your code, when in a testing 
  frame of mind and not a programming frame of mind, can sometimes cause you to 
  think new thoughts. You can question your own assumptions by methodically 
  stepping through your code, looking at the methods you call, rechecking their 
  documentation, and thinking. Here are some cases to watch for. "Impossible" cases
Often, it will appear that error returns are impossible. Doublecheck your assumptions. 
 This example shows a Java implementation of a common Unix idiom for handling 
  temporary files. 
File file = new File("tempfile");
FileOutputStream s;
try {
    // open the temp file.
    s = new FileOutputStream(file);
} catch (IOException e) {...}
// Make sure temp file will be deleted
file.delete();
 The goal is to make sure that a temporary file is always deleted, no matter 
  how the program exits. You do this by creating the temporary file, then immediately 
  deleting it. On Unix, you can continue to work with the deleted file, and the 
  operating system takes care of cleaning up when the process exits. A not-painstaking 
  Unix programmer might not write the code to check for a failed deletion. Since 
  she just successfully created the file, she must be able to delete it. This trick doesn't work on Windows. The deletion will fail because the file 
  is open. Discovering that fact is hard: as of August 2000, the Java documentation 
  did not enumerate the situations in which delete 
  could fail; it merely says that it can. Butperhapswhen in "testing 
  mode", the programmer might question her assumption. Since her code is 
  supposed to be "write once, run everywhere", she might ask a Windows 
  programmer when File.delete fails on
  Windows and so discover the 
  awful truth. "Irrelevant" cases
Another force against noticing a distinct relevant value is being already convinced 
  that it doesn't matter. A Java Comparator's 
  compare method returns either a number <0, 
  0, or a number >0. Those are three distinct cases that might be tried. This 
  code lumps two of them together: 
void allCheck(Comparator c) {
   ...
   if (c.compare(o1, o2) <= 0) {
      ...
   } else {
      ...
   } 
 But that might be wrong. The way to discover whether it is or not is to try 
  the two cases separately, even if you really believe it will make no difference. 
  (Your beliefs are really what you're testing.) Note that you might be executing 
  the then case of the if 
  statement more than once for other reasons. Why not try one of them with the 
  result less than 0 and one with the result exactly equal to zero? Uncaught exceptions
Exceptions are a kind of distinct result. By way of background, consider this 
  code: 
void process(Reader r) {
   ...
   try {
      ...
      int c = r.read();
      ...
   } catch (IOException e) {
      ...
   }
}
 You'd expect to check whether the handler code really does the right thing 
  with a read failure. But suppose an exception is explicitly unhandled. Instead, 
  it's allowed to propagate upward through the code under test. In Java, that 
  might look like this: 
void process(Reader r) throws IOException {
    ...
    int c = r.read();
    ...
}
 This technique asks you to test that case even though the code explicitly 
  doesn't handle it. Why? Because of this kind of fault: 
void process(Reader r) throws IOException {
    ...
    Tracker.hold(this);
    ...
    int c = r.read();
    ...
    Tracker.release(this);
    ...
}
 Here, the code affects global state (through Tracker.hold). 
  If the exception is thrown, Tracker.release 
  will never be called. (Notice that the failure to release will probably have no obvious immediate 
  consequences. The problem will most likely not be visible until process 
  is called again, whereupon the attempt to hold 
  the object for a second time will fail. A good article about such defects is 
  Keith Stobie's "Testing 
  for Exceptions".) This particular technique does not address all defects associated with method 
  calls. Here are two kinds that it's unlikely to catch. Incorrect arguments Consider these two lines of C code, where the first line is wrong and the second 
  line is correct. 
... strncmp(s1, s2, strlen(s1)) ...
... strncmp(s1, s2, strlen(s2)) ...
 strncmp compares two strings and returns a 
  number less than 0 if the first one is lexicographically less than the second 
  (would come earlier in a dictionary), 0 if they're equal, and a number greater 
  than 0 if the first one is lexicographically larger. However, it only compares 
  the number of characters given by the third argument. The problem is that the 
  length of the first string is used to limit the comparison, whereas it should 
  be the length of the second. This technique would require three tests, one for each distinct return value. 
  Here are three you could use: 
    
        | s1 | s2 | expected
        result | actual
        result |  
        | "a" | "bbb" | <0 | <0 |  
        | "bbb" | "a" | >0 | >0 |  
        | "foo" | "foo" | =0 | =0 |  The defect is not discovered because nothing in this technique forces 
  the third argument to have any particular value. What's needed is a test case 
  like this: 
    
        | s1 | s2 | expected
        result | actual
        result |  
        | "foo" | "food" | <0 | =0 |  While there are techniques suitable for catching such defects, they are seldom 
  used in practice. Your testing effort is probably better spent on a rich set 
  of tests that targets many types of defects (and that you hope catches this 
  type as a side effect). Indistinct results
There's a danger that comes when you're coding - and testing - method-by-method. 
  Here's an example. There are two methods. The first, connect, 
  wants to establish a network connection: 
void connect() {
   ...
   Integer portNumber = serverPortFromUser();
   if (portNumber == null) {
      // pop up message about invalid port number
      return;
   }
 It calls serverPortFromUser to get a port number. 
  That method returns two distinct values. It returns a port number chosen by 
  the user if the number chosen is valid (1000 or greater). Otherwise, it returns 
  null. If null is returned, the code under test pops up an error message and 
  quits. When connect was tested, it worked as intended: 
  a valid port number caused a connection to be established, and an invalid one 
  led to a popup. The code to serverPortFromUser is a bit more 
  complicated. It first pops up a window that asks for a string and has the standard 
  OK and CANCEL buttons. Based on what the user does, there are four cases: 
  If the user types a valid number, that number is returned.If the number is too small (less than 1000), null is returned (so the message 
    about invalid port number will be displayed).If the number is misformatted, null is again returned (and the same message 
    is appropriate). If the user clicks CANCEL, null is returned.  This code also works as intended.  The combination of the two chunks of code, though, has a bad consequence: the 
  user presses CANCEL and gets a message about an invalid port number. All the 
  code works as intended, but the overall effect is still wrong. It was tested 
  in a reasonable way, but a defect was missed. The problem here is that null is one result 
  that represents two distinct meanings ("bad value" and "user 
  cancelled"). Nothing in this technique forces you to notice that problem 
  with the design of serverPortFromUser. Testing can help, though. When serverPortFromUser 
  is tested in isolation - just to see if it returns the intended value in each 
  of those four cases - the context of use is lost. Instead, suppose it were tested 
  via connect. There would be four tests that 
  would exercise both of the methods simultaneously: 
    
        | input | expected
        result | thought
        process |  
        | user types "1000" | connection to port 1000 is
        opened | serverPortFromUser
        returns a number, which is used. |  
        | user types "999" | popup about invalid port number | serverPortFromUser
        returns null, which leads to popup |  
        | user
        types "i99" | popup about invalid port
        number | serverPortFromUser
        returns null, which leads to popup |  
        | users clicks CANCEL | whole connection process
        should be cancelled | serverPortFromUser
        returns null, hey wait a minute that doesn't make sense... |  As is often the case, testing in a larger context reveals integration problems 
  that escape small-scale testing. And, as is also often the case, careful thought 
  during test design reveals the problem before the test is run. (But if the defect 
  isn't caught then, it will be caught when the test is run.) 
 
 
Copyright 
© 1987 - 2001 Rational Software Corporation
 |