1.8 Testing and Debugging
Shplait includes built-in support for testing your programs. The check form takes two expressions and makes sure that they produce the same value, where the second expression is prefixed with ~is. Typically, the first expression is a function call, and the second expression is the expected result of the call. The check form is silent if a test passes, but it prints an error message (and then continues, anyway) if the test fails.
taste("milk")
~is #'good
- Void
taste("brussel sprouts")
~is #'not_as_good
- Void
taste("beets")
~is #'bad
- Void
check: failed
got: #’not_as_good
expected: #’bad
To test that an expression reports an expected error, use ~raises in place of ~is, and then supply an expression that produces a string after ~raises. The test checks that the exception’s error message contains the string.
always_fail(42)
~raises "not actually"
- Void
always_fail(42)
~raises "should not get called"
- Void
check: failed
got: exception always_fail: we’re not actually returning a number
expected: exception "should not get called"
When you write a program (in the definitions area of DrRacket), the order of function definitions generally does not matter, even if the functions call each other. A test at the top level of a program, however, must appear after all functions that the test may end up calling. To relax this constraint, wrap tests in a module test form. A module test wrapper effectively moves its content to the end of the program.
:«
module test:
retaste("milk") // use appears before definition of retaste
fun retaste(s):
[#'still, taste(s)]
// test module runs automatically after the definition
»
A good set of tests will cause all expressions in a program to be evaluated at least once. DrRacket can help you check that your program has good test coverage. In DrRacket’s Language menu, select Choose Language, click Show Details, and then select the Syntactic test suite coverage option. After selecting that option, when you Run a program, it will stay its normal color if all is well. If some expression has not been covered, however, the program text will go mostly black, and any expression that has not been evaluated will turn orange with a black background. Resolve the problem and restore your program text’s color by adding more tests.
When you’re debugging a program, it may be helpful to see the arguments that are passed to a particular function and the results that the function returns. You can enable that kind of tracing for a function with the trace form. Tracing is enabled until the expression in the body of trace returns a value.
> trace got_milk:
got_milk([])
- Boolean
=> got_milk([])
<= #false
#false
> trace got_milk:
got_milk(["cookies", "milk"])
- Boolean
=> got_milk(["cookies", "milk"])
=> got_milk(["milk"])
<= #true
#true
To trace more than one function, you can nest trace forms.
As you’re developing a program, sometimes it’s useful to run a partial program where you haven’t yet decided on part of the implementation. The .... operator (with four dots) can be used as a prefix, infix, or postfix operator. A program using .... can compile and run, but the .... reports an error if it is reached during evaluation.
match items
| []: #false
> got_milk([])
- Boolean
#false
> got_milk(["cheese"])
- Boolean
reached an incomplete expression