CSc 453: Some Thoughts on Testing

The problem of coming up with a good set of test cases is an important problem in software development, has been studied extensively in the software engineering literature, and has a wide variety of supporting tools available (see, e.g., http://www.testingfaqs.org). There is far too much on this topic "out there" to imagine that I can give any sort of reasonably thorough introduction to the topic here. This page aims, instead, to simply list a few guidelines that may be helpful in suggesting how to structure test cases. It has no pretensions to any sort of completeness. Caveat emptor.

Zero, One, Many

A very useful way to construct test cases is to consider a Zero-One-Many rule. The essential idea here is to have, for each feature X of interest, test cases that have: (a) zero occurrences of X; (b) one occurrence of X; and (c) many occurrences of X. For example, when testing the parsing of function prototypes, we might consider the following cases:

Similarly, to test the parsing of if statements, we might consider test cases with no ifs, a single if, several ifs one after another, and several ifs nested one within another.

Boundary Cases

Construct inputs to test any limiting values of your system. For example, what happens if the input is empty? If your implementation uses a buffer size of 1K, what happens if you get an identifier whose length exceeds that value? (Remember that implementation-specific limits on such things as the length of an identifier or the value of an integer are permissible, as long as the limits are "reasonable", but the implementation should behave gracefully on inputs that exceed those limits. Dumping core, or halting and catching fire, are not acceptable. Buffers that overflow on big inputs will be a source of delight to hackers.)

Testing Mechanics

Consider starting with a (large) number of test inputs, each of which tests one feature. Then construct a further (even larger) number of test inputs that explore any possible interactions between different features (such as the nesting of if and while statements, or an assignment whose left hand side contains intermingled array references and function calls).

Potentially, this means that you may be looking at dozens or even hundreds of test inputs. How do you manage them all? The simplest approach is to automate the testing, e.g., using a script. For example, my own test script looks something like this:

#!/bin/csh -fx
set TESTDIR = ...
set TESTS = (test01.c test02.c test03.c ...)
set BINARY = "./parse"

foreach t ( ${TESTS} )
   echo "TESTING: ${t}"
   if ( -e ${t}.out ) /bin/rm -f ${t}.out
   if ( -e ${t}.diff ) /bin/rm -f ${t}.diff
   ${BINARY} < ${TESTDIR}/${t} > ${t}.out
   if ( -e ${t}.out ) diff ${t}-expected.out ${t}.out > ${t}.diff
   if ( -e ${t}.diff && -z ${t}.diff) then
      echo "TEST ${t} : PASSED"
   else
      echo "TEST ${t} : FAILED"
   endif
end

The Relationship of C-- to C

The programming language for which you are building a compiler, C--, is a subset of C. This means that every legal C-- program is also a legal C program, but some legal C programs are not legal C-- programs. You can sometimes take advantage of this by using GCC to do some of your work for you: