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.

fun taste(s):

  cond

  | s == "milk": #'good

  | ~else: #'not_as_good

check:

  taste("milk")

  ~is #'good

- Void

check:

  taste("brussel sprouts")

  ~is #'not_as_good

- Void

check:

  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.

fun always_fail(n :: Int) :: Int:

  error(#'always_fail, "we're not actually returning a number")

check:

  always_fail(42)

  ~raises "not actually"

- Void

check:

  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:

    check:

      retaste("milk") // use appears before definition of retaste

      ~is [#'still, #'good]

 

  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.

fun got_milk(items :: Listof(String)):

  match items

  | []: #false

  | cons(item, rst_items): item == "milk" || got_milk(rst_items)

> 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.

fun got_milk(items :: Listof(String)):

  match items

  | []: #false

  | cons(item, rst_items): .... not sure ....

> got_milk([])

- Boolean

#false

> got_milk(["cheese"])

- Boolean

exn:fail:syntax: contract violation

  expected: (listof syntax?)

  given: [’....’]