So on my journey with Racket I Needed a Mock - Again

I almost copied this almost verbatim from my blog post about mocking a DB connection.

Some things are simple. Files are not one of them. They're brittle, stateful, side-effect ridden… Any number of crimes against simplicity 😃 So they're excellent candidates for mocking. The documentation for the racket mock package talks about it a little but I figured I'd just post this little example to help others. And of course me when I can't remember how this works any more.

The big difference here is that I am mocking the output of a lambda wrapped in a form rather than a form directly.

So, here's a little function that updates an HTML report. The idea is that I find the bottom of the list add in an additional item and write the file back. It's not fancy and it wouldn't scale for large files, but it works for right now. Note, it's got zero error handling or any of the good stuff:

(define (update-html-summary-report table-name location
                                  #:input-file [open-input-file open-input-file]
                                  #:output-file [call-with-output-file call-with-output-file])
(define summary-location "index.html")
(define report
  (port->string (open-input-file summary-location) #:close? #t))
(define list-item (string-append "<li><a href=\"" location "\">" table-name "</a></li>"))
(define list-bottom (string-replace report "</ul>" (string-append list-item "</ul>")))

(call-with-output-file summary-location
  (lambda (out)
    (display list-bottom out)) #:exists 'replace)

There's a couple of other obvious refactoring opportunities here. For instance note that at the end of the form I'm just returning the true value #t. Which doesn't give much by the way of context for success. Also I replace the string </ul> with a larger string including the string </ul>… I'm sure with thought I can achieve better.

Now initial testing proved rather unsatisfying. I could mock the input quickly and effectively. But the output is key here. In effect call-with-output-file is going to run the lambda it is passed and I want to know what's passed to the output port.

So, my initial tests used check-mock-num-calls to ensure the mock output form was called. But that doesn't tell you what the output port will receive. Somewhat glumly reading the mock docs I stumbled over mock-call-results. Now this is handy! mock-call-results returns a list that allows you to see the results of calling a given mock. Bingo 🎉

That provides you the ability to write a mock which calls the lambda and returns the result.

(module+ test
  (define mock-report (lambda (filename) (open-input-string "<ul></ul>")))
  (define input-file-mock (mock #:behavior mock-report))
  (define mock-outputter (lambda (filename output-proc #:exists exists-val)
                           (define mock-port (open-output-string))
                           (output-proc mock-port)
                           (get-output-string mock-port)))
  (define output-file-mock (mock #:behavior mock-outputter))

  (test-case "update-html-summary-report opens index.html"
    (update-html-summary-report "test" "test.html" #:input-file input-file-mock #:output-file output-file-mock)
    (check-mock-called-with? input-file-mock (arguments "index.html")))
  (test-case "update-html-summary-report writes index.html but only once"
    (mock-reset! input-file-mock)
    (mock-reset! output-file-mock)
    (update-html-summary-report "test" "test.html" #:input-file input-file-mock #:output-file output-file-mock)
    (check-mock-num-calls output-file-mock 1))
  (test-case "update-html-summary-report puts the report back"
    (mock-reset! input-file-mock)
    (mock-reset! output-file-mock)
    (define expectation "<ul><li><a href=\"test.html\">test</a></li></ul>")
    (update-html-summary-report "test" "test.html" #:input-file input-file-mock #:output-file output-file-mock)
    (define result (car (mock-call-results (car (mock-calls output-file-mock)))))
    (check-equal? result expectation)))

I'm really starting to appreciate the mock package. It has a neat design that makes hard things possible. Thanks Jack Firth.