TIL: Enforcing contracts with `contract-out` from within an embedded test module

TL;DR :wink:

It's possible to enforce and test contracts defined at the module boundary within a test module in the same file. Do this by require'ing (submod "..") in the test module:

(module+ test
  (require
    ...
    (submod ".."))

  ...)

Found at Confused about racket contracts - Stack Overflow


Discussion

Usually my modules are of the form

#lang racket/base

(require racket/contract)

(provide
  (contract-out
    ...)
)

; Definitions
(define ...)

; Tests
(module+ test
  ...
)

Testing the module mostly works fine, however the contracts defined with contract-out aren't applied in the tests. It would be possible to work around this by defining module contents with define/contract etc., but this has the disadvantage that the contracts are checked for all uses in the module which might slow down the code too much.

When I was looking for a way to test the contracts defined with contract-out from within the same file, I found the above StackOverflow answer. Due to the (submod "..") import in the test module it accesses the module in the same file across the file's module boundary.

1 Like

Another tip related to testing contracts: If a contract is relatively complicated and especially if it's difficult to test procedures where the contract is used, it may make sense to put the contract in its own predicate function and test this function.

For example, today I wrote this code:

(define omit-fields/c
  (and/c (listof (or/c 'completed? 'priority 'completion-date 'creation-date))
         (lambda (omit-fields)
           ; These aren't actual dates, but they work the same way for
           ; `task-date-combination/c`.
           (define completion-date (index-of omit-fields 'completion-date))
           (define creation-date (index-of omit-fields 'creation-date))
           ; In the context of `task->string` we're interested in omitting the
           ; fields, i.e. _not_ having them.
           (task-date-combination/c (not completion-date) (not creation-date)))))

(the concrete background isn't relevant here), and I test omit-fields/c on its own in the tests module:

  ...
  (run-tests
    (test-suite "Contracts"
      ; `task-date-combination/c`
      (test-case
        "Valid date combinations"
        (check-true (task-date-combination/c #f #f))
        (check-true (task-date-combination/c #f "2022-01-04"))
        (check-true (task-date-combination/c "2022-01-04" "2022-01-03")))
      (test-false
        "Completion date without creation date"
        (task-date-combination/c "2022-01-04" #f))
      ; `omit-fields/c`
      (test-false
        "Invalid task field symbol"
        (omit-fields/c '(priority invalid)))
      (test-true
        "Omit both dates"
        (omit-fields/c '(priority creation-date completion-date)))
      (test-true
        "Omit completion date, but keep creation date"
        (omit-fields/c '(priority completion-date)))
      (test-true
        "Omit neither date"
        (omit-fields/c '(priority)))
      (test-false
        "Omit creation date, but keep completion date"
        (omit-fields/c '(priority creation-date)))
  ))
  ...