Home

Awesome

(provide prefix-for-test-report ; parameter printed at start of each test prefix-for-diag ; parameter printed at front of each (diag ...) message

 diag			; print a diagnostic message. see prefix-for-diag
 
     ok                ; A value is true
     not-ok            ; A value is false
     is-false          ; Alias for not-ok

 is                ; A value is what it should be
     isnt              ; ...or is not what it shouldn't be

     like              ; A value matches a regex
     unlike            ; A value does not match a regex
 
     lives             ; expression does not throw an exception
     dies              ; expression does throw an exception
     throws            ; throws an exception and that exn matches a predicate
 
     matches           ; value matches a predicate
     not-matches       ; ...or doesn't
     is-type           ; alias for matches
     isnt-type         ; alias for not-matches
 
     is-approx         ; value is roughly the expected value
     isnt-approx       ; ...or not
 
     test-suite        ; gather a set of tests together, trap exns,
                   ;  output some extra debugging
		   
     make-test-file    ; create a file on disk, populate it
 
     expect-n-tests    ; unless N tests ran, print error at end of file execution
     done-testing      ; never mind how many tests we expect, we're okay if we see this
 
     test-more-check   ; fundamental test procedure.  All others call this
 
     ;  You generally should not be using these, but you can if you want
     current-test-num  ; return current test number
 next-test-num	   ; return and (optionally & by default) increment next test number
 
     ; reduce the test number by N (default: 1).  Mostly needed for running inner tests
 decrement-test-num! 

;;====================================================================== ;; The racket testing module has a few things I wish it did differently: ;; ;; 1) The test function names are verbose and redundant. check-this, check-that, etc ;; ;; 2) The test functions display nothing on success. There's no way ;; to tell the difference between "no tests ran" and "all tests ;; succeeded" ;; ;; 3) The tests return nothing. You can't do conditional tests like: ;; (when (is os 'macos) (ok mac-specific-tests)) ;; ;; ;; This module addresses those problems. It's named for, and largely a ;; clone of, the Test::More library on Perl's CPAN, although some ;; of the Perl version's features are not implemented. ;; ;; http://search.cpan.org/~exodist/Test-Simple-1.302120/lib/Test/More.pm ;; for more details on the original. ;; ;; TODO: ;; - Add 'disable this test suite' keyword ;; - Add 'TODO this test suite' keyword ;; - Fix the issue where it sometimes shows 'expected #f got #f' ;; - On test-suite, add 'setup' and 'cleanup' keywords that take thunks ;;====================================================================== ; Parameter: prefix-for-test-report ; Default: "" ; ; Set this to put a prefix on some or all of your tests. Example: ; ; (parameterize ([prefix-for-test-report "TODO: "]) ; ...tests...) ;;---------------------------------------------------------------------- ; Parameter: expect-n-tests ; Default : #f ; ; Set this to say "this script will run 17 tests" (or however many). ; If it runs more or fewer then an error will be reported at the end. ; ; See also: done-testing ; ; If neither this nor done-testing are seen before end of file, a ; warning will be reported when the tests are run. ; ; Typically, if you use this you would set it at top of file and then ; not modify it. One reason that you might change it pater would be ; if you had some conditional tests that you determined should be ; skipped. ; ;---------------------------------------------------------------------- ; ; Functions associated with the test number. ; ; In normal cases, there is no reason to touch these. ; ; ; parameter that tracks current number ; current-test-num
; ; ; Get and, by default, increment the test number by 1 ; (next-test-num #:inc [should-increment #t])
; ;;---------------------------------------------------------------------- ; test-more-check ; ; All the testing functions defined below are wrappers around this. ; ; (test-more-check #:got got ; the value to check ; #:expected [expected #t] ; what it should be ; #:msg [msg ""] ; what message to display ; #:op [op equal?] ; (op got expected) determines success ; #:show-expected/got? [show-expected/got? #t] ; display expected and got on fail? ; #:report-expected-as [report-expected-as #f] ; show this as expected on fail ; #:report-got-as [report-got-as #f] ; show this as got on fail ; #:return [return #f] ; return value ; ; The 'got' value will be sent through handy/utils 'unwrap-val' ; function before use, meaning that: ; ; - If it's a procedure, it will be executed with no arguments ; - If it's a promise, it will be forced ; - If it's anything else, it will be used as is ; ; In the first two cases, whatever is returned will be the actual ; value used. ; ;;---------------------------------------------------------------------- ; (ok got [msg ""]) ; (not-ok got [msg ""]) ; opposite of ok ; (is-false got [msg ""]) ; alias for not-ok; reads better in some cases ; ; Simple boolean check. Was the value of 'got' true? (i.e., it wasn't #f) ; (ok 7) ; success. returns 7. prints just the normal "ok <test-num>" banner ; (ok #f) ; fail. returns #f ; (ok 7 "foo") ; success, returns 7, prints "ok <test-num> - foo" ; ; (not-ok #f) ; success. Returns #t on success, #f on failure ; (is-false #f) ; same as previous ; ;;---------------------------------------------------------------------- ; (matches val predicate [msg ""] [op equal?]) ; (not-matches val predicate [msg ""] [op equal?]) ; opposite of matches ; ; is-type ; alias for matches ; isnt-type ; alias for not-matches ; ; Verify that the value of 'got' does / does not match the predicate ; ; (matches (my-func) hash? "(my-func) returns a hash") ; (is-type (my-func) hash? "(my-func) returns a hash") ; ; (not-matches 'foo hash? "symbol foo is not a hash") ; (isnt-type 'foo hash? "symbol foo is not a hash") ; ;;---------------------------------------------------------------------- ; (is val expected [msg ""] <optional comparison func> #:op <optional comparison func>) ; (isnt val expected [msg ""] <optional comparison func> #:op <optional comparison func>) ; ; (is x 8 "x is 8") ; (is (myfunc 7) 8 "(myfunc 7) returns 8") ; (is x 8 "x is 8" =) ; use = instead of equal? for comparison ; (is x 8 "x is 8" #:op =) ; use = instead of equal? for comparison ; ; The bread and butter of test-more. Asks if two values are / not the same ; according to a particular comparison operator. (by default 'equal?') ; ; Returns the value that was checked (i.e. 'val', the first argument) ; ; NOTE: You can specify the comparison operator either positionally or ; via a keyword. The ability to provide an operator was added after ; this was already in use in code. It was originally added as an ; optional parameter, and the better idea of having it be a keyword ; came along last. In order to maintain backwards compatibility, both ; are supported. If both are provided then the positional one wins. ; ;;---------------------------------------------------------------------- ; (like val regex [msg ""]) ; Returns the result of the regexp match ; (unlike val regex [msg ""]) ; Returns #t or #f ; ; Checks that the value does/doesn't match a regex. ; ;;---------------------------------------------------------------------- ; (lives thnk [msg ""]) ; ; Verify that a thunk will run without throwing an exception. The ; thunk may contain other tests. ; ;;---------------------------------------------------------------------- ; (throws thnk pred [msg ""]) ; (define/contract (throws thnk pred [msg ""] #:strip-message? [strip-message? #t]) ; ; Verify that a thunk DOES throw an exception and that the exception ; matches a specified predicate. ; ; 'pred' could be anything, but some types are handled specially: ; - string: Check if it is exactly the (perhaps stripped) exn message ; - proc: Pass it the exn, see if it returns #t ; - regex: Check if the regex matches the (exn message || string) thrown ; - etc: Check if it's equal? to the exception ; ; NOTE: If you give it a function predicate that predicate must take ; one argument but it can be anything, not just an (exn?) ; ; NOTE: When providing a string as the value, it is matched against ; the exception message (assuming there is an exception). ; If #:strip-message? is true (the default) then everything up to the ; first "expected: " is snipped off, as is everything after the last \n ; ;;---------------------------------------------------------------------- ; (dies thnk [msg ""]) ; ; Use this when all you care about is that it dies, not why. ; ;;---------------------------------------------------------------------- ; (test-suite ...) ; ; Group a bunch of tests together and give them an identity. Trap ; exceptions that they throw and report on whether they threw. Print ; header and footer banners so it's easy to tell where they ; start/finish. Returns (void) ; ; (test-suite ; "user creation" ; ; (lives (thunk (my-list)) "(my-list) lives") ; (is (my-list) '() "(my-list) returns '()") ; (is (my-list 7) '(7) "(my-list 7) returns '(7)") ; ) ; ; The above code prints: ; ; ### START test-suite: user creation ; ok 1 - (my-list) lives ; ok 2 - (my-list) returns '() ; ok 3 - (my-list 7) returns '(7) ; ; Total tests passed so far: 3 ; Total tests failed so far: 0 ; ; ### END test-suite: user creation ; ;;---------------------------------------------------------------------- ; (define/contract (make-test-file [fpath (make-temporary-file)] ; [text (rand-val "test file contents")] ; #:overwrite [overwrite #t]) ; (->* () (path-string? string? #:overwrite boolean?) path-string?) ; ; Creates (and, optionally, populates) a file for use by a test. ; ; If fpath is not specified it will default. See make-temporary-file ; in the Racket docs for details. ; ; If fpath is an existing directory, a file with a random name will be ; created in that directory. ;; ; If fpath is a filepath and its directory does not exist then it will ; be created. ; ; Once we have decided on the filepath according to the above details, ; we check to see if the file exists. If so, make-test-file will ; either throw an exception or overwrite the existing file depending ; on the value of 'overwrite'. DEFAULT IS TO OVERWRITE because you're ; generating a file for testing and it's assumed that you know what ; you're doing. ; ; Note: Once you're done with your tests, you will need to manually ; delete the file that this creates unless you do something like this: ; ; (require handy/utils) ; (with-temp-file #:path (make-test-file) ; (lambda (filepath) ; ...the test file is at 'filepath' and has been created and populated... ; ) ; ) ; ; After leaving the scope of the 'with-temp-file', the test file is ; ; guaranteed to have been deleted because that's what with-temp-file does ; ; The file will be populated with the text you specify, or with some ; random text if you don't specify anything. (Note that it's written ; via 'display', but you can use (make-test-file #:text (~v <data>)) ; if that's what you want. ; ;;---------------------------------------------------------------------- ; (done-testing) ; ; It can be a pain to count exactly how many tests you're going to ; run, especially if some of the tests are conditional. If you simply ; put (done-testing) as the last line in your test file then test-more ; will assume that you completed correctly. ; ; If neither this nor expect-n-tests are seen before end of file, a ; warning will be reported when the tests are run. ; ; If both this and expect-n-tests are run, this wins; it will not ; check how many tests were run. ; ;;---------------------------------------------------------------------- ; (diag . args) ; ; Variadic print statement that outputs the specified items with a ; standard prefix, stored in the 'prefix-for-diag' parameter. By ; default this is "\t#### ", that's easy for test output analyzers to ; detect. This prefix is prepended to the current value of the ; prefix-for-say parameter, so this: ; ; (parameterize ([prefix-for-diag "my awesome message is: "]) ; (diag "foobar")) ; ; ...is the same as (displayln "\t#### my awesome message is: foobar") ; ; NB: This value is actually prepended to the prefix-for-say from ; handy/utils. That's normally "", but if it's been set then you'll ; see something different than stated above. ; ;;---------------------------------------------------------------------- ; (is-approx got expected [msg ""] ; #:threshold [threshold 1] ; #:key [key identity] ; #:compare [compare #f] ; value based on threshold ; #:abs-diff? [abs-diff? #t]) ; (->* (any/c any/c) ; (string? ; #:threshold any/c ; #:key (-> any/c any/c) ; (key got) (key expected) ; #:compare (-> any/c any/c any/c) ; (compare diff threshold) ; #:diff-with (-> any/c any/c any/c) ; (diff-with got expected) ; #:abs-diff? boolean?) ; use abs on diff before compare ; any/c) ; ; threshold How close do they need to be? Default is 1. ; key Function that generates a (usually numeric, but could be anything) ; value from each of 'got', 'expected' ; compare two arguments, diff and threshold, returning anything. true ; return value means success ; abs-diff? Use the absolute value of the difference? Determines the acceptable ranges. ; ; Test that two values ('got' and 'expected') are approximately the ; same within a certain threshold. ; ; 'got' and 'expected' can be anything, but will usually be numbers. ; You may provide a 'key' function that generates a new value based on ; the value of 'got' and 'expected' ; ; If you don't provide a #:compare function, then it will assume that ; the values are numeric and the default acceptable ranges will depend ; on the value of threshold and abs-diff?: ; ; abs-diff? threshold default value for compare (argument is diff or abs(diff)) ; #t/#f 0 (= 0 diff) ; #t != 0 (<= diff (abs threshold)) ; #f < 0 ((between/c threshold 0) diff) ; #f > 0 ((between/c 0 threshold) diff) ; ; Examples: ; ; (define now (current-seconds)) ; epoch time ; (is-approx (and (myfunc) (current-seconds)) now "(myfunc) ran in no more than 1 second") ; ; (for ([num (in-range 3 7)]) ; (let ([myfunc (thunk (make-list num 'x))]) ; (is-approx (length (myfunc)) ; 3 ; #:threshold 3 ; #:abs-diff? #f ; "(myfunc) => list of 3-6 elements"))) ; (is-approx (hash 'age 8) ; (hash 'age 9) ; #:key (curryr hash-ref 'age) ; "age is about 9") ; ; ; More complex examples: ; (is-approx ((thunk "Foobar")) ; "f" ; #:key (compose1 char->integer (curryr string-ref 0) string-downcase) ; "(myfunc) returns a string that starts with 'f', 'F', 'g', or 'G'") ; ; (is-approx (hash 'username "tom") ; (hash 'username "tomas") ; #:key (curryr hash-ref 'username) ; #:abs-diff? #f ; #:diff-with (lambda (got expected) (regexp-match (regexp got) expected)) ; #:compare (lambda (diff threshold) (not (false? diff))) ; "first username matched part of second username") ; ;;---------------------------------------------------------------------- ; (define/contract (isnt-approx got expected [msg ""] ; #:threshold [threshold 1] ; #:key [key identity] ; #:compare [compare #f] ; value based on threshold ; #:abs-diff? [abs-diff? #t]) ; (->* (any/c any/c) ; (string? ; #:threshold any/c ; #:key (-> any/c any/c) ; (key got) (key expected) ; #:compare (-> any/c any/c any/c) ; (compare diff threshold) ; #:diff-with (-> any/c any/c any/c) ; (diff-with got expected) ; #:abs-diff? boolean?) ; use abs on diff before compare ; any/c) ; ; Same as is-approx but tests that it's outside the threshold ; ;;----------------------------------------------------------------------