racket/collects/schemeunit/scribblings/check.scrbl

346 lines
11 KiB
Racket

#lang scribble/doc
@(require "base.ss")
@title{Checks}
Checks are the basic building block of SchemeUnit. A check
checks some condition. If the condition holds the check
evaluates to @scheme[#t]. If the condition doesn't hold the
check raises an instance of @scheme[exn:test:check] with
information detailing the failure.
Although checks are implemented as macros, which is
necessary to grab source location, they are conceptually
functions. This means, for instance, checks always evaluate
their arguments. You can use check as first class
functions, though you will lose precision in the reported
source locations if you do so.
The following are the basic checks SchemeUnit provides. You
can create your own checks using @scheme[define-check].
@defproc[(check (op (-> any any any))
(v1 any)
(v2 any)
(message string? ""))
any]{
The simplest check. Succeeds if @scheme[op] applied to @scheme[v1] and @scheme[v2] is not @scheme[#f], otherwise raises an exception of type @scheme[exn:test:check]. The optional @scheme[message] is included in the output if the check fails. If the check succeeds, the value returned by @scheme[op] is the value returned by the check.}
For example, the following check succeeds:
@schemeblock[
(check < 2 3)
]
@defproc*[([(check-eq? (v1 any) (v2 any) (message string? "")) #t]
[(check-not-eq? (v1 any) (v2 any) (message string? "")) #t]
[(check-eqv? (v1 any) (v2 any) (message string? "")) #t]
[(check-equal? (v1 any) (v2 any) (message string? "")) #t]
[(check-not-equal? (v1 any) (v2 any) (message string? "")) #t])]{
Checks that @scheme[v1] is (not) @scheme[eq?],
@scheme[eqv?], or @scheme[equal?] to @scheme[v2]. The
optional @scheme[message] is included in the output if the
check fails.}
For example, the following checks all fail:
@schemeblock[
(check-eq? (list 1) (list 1) "allocated data not eq?")
(check-not-eq? 1 1 "integers are eq?")
(check-eqv? 1 1.0 "not eqv?")
(check-equal? 1 1.0 "not equal?")
(check-not-equal? (list 1) (list 1) "equal?")
]
@defproc[(check-pred (pred (-> any any)) (v any) (message string? ""))
#t]{Checks that @scheme[pred] returns a value that is not @scheme[#f] when applied to @scheme[v]. The optional @scheme[message] is included in the output if the check fails. The value returned by a successful check is the value returned by @scheme[pred].}
Here's an example that passes and an example that fails:
@schemeblock[
(check-pred string? "I work")
(check-pred number? "I fail")
]
@defproc[(check-= (v1 any) (v2 any) (epsilon number?) (message string? "")) #t]{
Checks that @scheme[v1] and @scheme[v2] are within
@scheme[epsilon] of one another. The optional
@scheme[message] is included in the output if the check
fails.}
Here's an example that passes and an example that fails:
@schemeblock[
(check-= 1.0 1.01 0.01 "I work")
(check-= 1.0 1.01 0.005 "I fail")
]
@defproc*[([(check-true (v any) (message string? "")) #t]
[(check-false (v any) (message string? "")) #t]
[(check-not-false (v any) (message string? "")) #t])]{
Checks that @scheme[v] is @scheme[#t], @scheme[#f], or not
@scheme[#f] as appropriate. The optional @scheme[message]
is included in the output if the check fails.}
For example, the following checks all fail:
@schemeblock[
(check-true 1)
(check-false 1)
(check-not-false #f)
]
@defproc[(check-exn (exn-predicate (-> any (or/c #t #f))) (thunk (-> any)) (message string? ""))
#t]{
Checks that @scheme[thunk] raises an exception for which
@scheme[exn-predicate] returns @scheme[#t]. The optional
@scheme[message] is included in the output if the check
fails. A common error is to use an expression instead of a
function of no arguments for @scheme[thunk]. Remember that
checks are conceptually functions.}
Here are two example, one showing a test that succeeds, and one showing a common error:
@schemeblock[
(check-exn exn?
(lambda ()
(raise (make-exn "Hi there"
(current-continuation-marks)))))
(code:comment "Forgot to wrap the expression in a thunk. Don't do this!")
(check-exn exn?
(raise (make-exn "Hi there"
(current-continuation-marks))))
]
@defproc[(check-not-exn (thunk (-> any)) (message string? "")) #t]{
Checks that @scheme[thunk] does not raise any exceptions.
The optional @scheme[message] is included in the output if
the check fails.}
@defproc[(fail (message string? "")) #t]{This checks fails unconditionally. Good for creating test stubs that youintend to fill out later. The optional @scheme[message] is included in the output if the check fails.}
@defproc[(check-regexp-match (regexp regexp?) (string string?)) #t]{Checks that @scheme[regexp] matches the @scheme[string].}
The following check will succeed:
@schemeblock[(check-regexp-match "a+bba" "aaaaaabba")]
This check will fail:
@schemeblock[(check-regexp-match "a+bba" "aaaabbba")]
@section{Augmenting Information on Check Failure}
When an check fails it stores information including the name
of the check, the location and message (if available), the
expression the check is called with, and the parameters to
the check. Additional information can be stored by using
the @scheme[with-check-info*] function, and the
@scheme[with-check-info] macro.
@defstruct[check-info ([name symbol?] [value any])]{
A check-info structure stores information associated
with the context of execution of an check.}
The are several predefined functions that create check
information structures with predefined names. This avoids
misspelling errors:
@defproc*[([(make-check-name (name string?)) check-info?]
[(make-check-params (params (listof any))) check-info?]
[(make-check-location (loc (list/c any (or/c number? #f) (or/c number? #f)
(or/c number? #f) (or/c number? #f))))
check-info?]
[(make-check-expression (msg any)) check-info?]
[(make-check-message (msg string?)) check-info?]
[(make-check-actual (param any)) check-info?]
[(make-check-expected (param any)) check-info?])]{}
@defproc[(with-check-info* (info (listof check-info?)) (thunk (-> any))) any]{
Stores the given @scheme[info] on the check-info stack for
the duration (the dynamic extent) of the execution of
@scheme[thunk]}
Example:
@schemeblock[
(with-check-info*
(list (make-check-info 'time (current-seconds)))
(lambda () (check = 1 2)))
]
When this check fails the message
@verbatim{time: <current-seconds-at-time-of-running-check>}
will be printed along with the usual information on an
check failure.
@defform[(with-check-info ((name val) ...) body ...)]{
The @scheme[with-check-info] macro stores the given
information in the check information stack for the duration
of the execution of the body expressions. @scheme[Name] is
a quoted symbol and @scheme[val] is any value.}
Example:
@schemeblock[
(for-each
(lambda (elt)
(with-check-info
(('current-element elt))
(check-pred odd? elt)))
(list 1 3 5 7 8))
]
When this test fails the message
@verbatim{current-element: 8}
will be displayed along with the usual information on an
check failure.
@section{Custom Checks}
Custom checks can be defined using @scheme[define-check] and
its variants. To effectively use these macros it is useful
to understand a few details about a check's evaluation
model.
Firstly, a check should be considered a function, even
though most uses are actually macros. In particular, checks
always evaluate their arguments exactly once before
executing any expressions in the body of the checks. Hence
if you wish to write checks that evalute user defined code
that code must be wrapped in a thunk (a function of no
arguments) by the user. The predefined @scheme[check-exn]
is an example of this type of check.
It is also useful to understand how the check information
stack operates. The stack is stored in a parameter and the
@scheme[with-check-info] forms evaluate to calls to
@scheme[parameterize]. Hence check information has lexical
scope. For this reason simple checks (see below) cannot
usefully contain calls to @scheme[with-check-info] to report
additional information. All checks created using
@scheme[define-simple-check] or @scheme[define-check] grab
some information by default: the name of the checks and the
values of the parameters. Additionally the macro forms of
checks grab location information and the expressions passed
as parameters.
@defform[(define-simple-check (name param ...) expr ...)]{
The @scheme[define-simple-check] macro constructs a check
called @scheme[name] that takes the params and an optional
message as arguments and evaluates the @scheme[expr]s. The
check fails if the result of the @scheme[expr]s is
@scheme[#f]. Otherwise the check succeeds. Note that
simple checks cannot report extra information using
@scheme[with-check-info].}
Example:
To define a check @scheme[check-odd?]
@schemeblock[
(define-simple-check (check-odd? number)
(odd? number))
]
We can use these checks in the usual way:
@schemeblock[
(check-odd? 3) (code:comment "Success")
(check-odd? 2) (code:comment "Failure")
]
@defform*[[(define-binary-check (name pred actual expected))
(define-binary-check (name actual expected) expr ...)]]{
The @scheme[define-binary-check] macro constructs a check
that tests a binary predicate. It's benefit over
@scheme[define-simple-check] is in better reporting on check
failure. The first form of the macro accepts a binary
predicate and tests if the predicate holds for the given
values. The second form tests if @scheme[expr] non-false.
}
Examples:
Here's the first form, where we use a predefined predicate
to construct a binary check:
@schemeblock[
(define-binary-check (check-char=? char=? actual expected))
]
In use:
@schemeblock[
(check-char=? (read-char a-port) #\a)
]
If the expression is more complicated the second form should
be used. For example, below we define a binary check that
tests a number if within 0.01 of the expected value:
@schemeblock[
(define-binary-check (check-in-tolerance actual expected)
(< (abs (- actual expected)) 0.01))
]
@defform[(define-check (name param ...) expr ...)]{
The @scheme[define-check] macro acts in exactly the same way
as @scheme[define-simple-check], except the check only fails
if the macro @scheme[fail-check] is called in the body of
the check. This allows more flexible checks, and in
particular more flexible reporting options.}
@defform[(fail-check)]{The @scheme[fail-check] macro raises an @scheme[exn:test:check] with
the contents of the check information stack.}
@section{The Check Evaluation Context}
The semantics of checks are determined by the parameters
@scheme[current-check-around] and
@scheme[current-check-handler]. Other testing form such as
@scheme[test-begin] and @scheme[test-suite] change the value
of these parameters.
@defparam[current-check-handler handler (-> any/c any/c)]{
Parameter containing the function that handles exceptions
raised by check failures. The default behaviour is to print
an error message including the exception message and stack
trace. }
@defparam[current-check-around check (-> thunk any/c)]{
Parameter containing the function that handles the execution
of checks. The default value wraps the evaluation of
@scheme[thunk] in a @scheme[with-handlers] call that calls
@scheme[current-check-handler] if an exception is raised and then
(when an exception is not raised) discards the result, returning
@scheme[(void)].
}