1.8 Testing and Debugging🔗ℹ

Plait includes built-in support for testing your programs. The test form takes two expressions and makes sure that they produce the same value. Typically, the first expression is a function call, and the second expression is the expected result of the function. The test form prints output that starts “good” if the test passes or “bad” if it fails.

> (define (taste s)
    (cond
      [(equal? s "milk") 'good]
      [else 'not-as-good]))
> (test (taste "milk") 'good)

- Void

good (taste "milk") at line 162

  expected: 'good

  given: 'good

> (test (taste "brussel sprouts") 'not-as-good)

- Void

good (taste "brussel sprouts") at line 163

  expected: 'not-as-good

  given: 'not-as-good

> (test (taste "beets") 'bad)

- Void

bad (taste "beets") at line 164

  expected: 'bad

  given: 'not-as-good

They say that no news is good news. To suppress the output for passing tests, so that only failing test strigger output, use (print-only-errors #t).

> (print-only-errors #t)

- Void

> (test (taste "kale") 'not-as-good)

- Void

> (test (taste "anchovies") 'bad)

- Void

bad (taste "anchovies") at line 167

  expected: 'bad

  given: 'not-as-good

To test that an expression reports an expected error, use test/exn. The test/exn form’s section expression should produce a string, and test/exn checks that an error is reported where the string occurs in the error message. You can only test for errors that your program specifically reports using the error function.

> (define (always-fail [n : Number]) : Number
    (error 'always-fail "we're not actually returning a number"))
> (test/exn (always-fail 42) "not actually")

- Void

> (test/exn (always-fail 42) "should not get called")

- Void

bad (always-fail 42) at line 170

  expected: "should not get called"

  given: "always-fail: we're not actually returning a number"

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
  (test (retaste "milk") '(still good)))
 
(define (retaste s)
  (list 'still (taste s)))

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, click Submodules to run, 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 declaration, which must appear after the function’s definitions.

(define (got-milk? [items : (Listof String)])
  (type-case (Listof String) items
    [empty #f]
    [(cons item rst-items) (or (string=? item "milk")
                               (got-milk? rst-items))]))
(trace got-milk?)
> (got-milk? empty)

- Boolean

>(got-milk? '())

<#f

#f

> (got-milk? '("cookies" "milk"))

- Boolean

>(got-milk? '("cookies" "milk"))

>(got-milk? '("milk"))

<#t

#t

As you’re devloping a program, sometimes it’s useful to run a partial program where you haven’t yet decided on part of the implementation. The .... expression (with four dots) can be used in place of any expression of any type. A program using .... can compile and run, but the .... reports an error if it is reached during evaluation.

> (define (got-milk? [items : (Listof String)])
    (type-case (Listof String) items
      [empty #f]
      [(cons item rst-items) ....]))

- Void

> (got-milk? '())

- Boolean

#f

> (got-milk? '("cheese"))

- Boolean

reached a `....` placeholder