cl-mock

2022-11-07

Mocking library

Upstream URL

github.com/Ferada/cl-mock

Author

Olof-Joachim Frahm <olof@macrolet.net>

License

AGPL-3+
README

-- mode: markdown; coding: utf-8-unix; --

CL-MOCK - Mocking functions.

Copyright (C) 2013-16 Olof-Joachim Frahm

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/.

Working, but unfinished.

Build Status

Portable to at least ABCL, Allegro CL (with one problem with inlining settings), SBCL, CCL and CLISP. CMUCL possibly, but not tested on Travis CI. ECL fails on Travis CI (TRIVIA fails there as well), but runs successfully on my own machine, so YMMV. See the detailed reports at https://travis-ci.org/Ferada/cl-mock for more information and CL-TRAVIS, and .travis.yml for the setup.

INTRODUCTION

This small library provides a way to replace the actual implementation of either regular or generic functions with mocks. On the one hand how to integrate this facility with a testing library is up to the user; the tests for the library are written in FIVEAM though, so most examples will take that into account. On the other hand writing interactions for mocks usually relies on a bit of pattern matching, therefore the regular CL-MOCK package relies on TRIVIA to provide that facility instead of deferring to the user. Should this be a concern a reduced system definition is available as CL-MOCK-BASIC, which excludes the definition of ANSWER and the dependency on TRIVIA.

Since it is pretty easy to just roll something like this on your own, the main purpose is to develop a nice (lispy, declarative) syntax to keep your tests readable and maintainable.

Some parts may be used independently of the testing facilities, e.g. dynamic FLET may be of general interest.

MOCKING REGULAR FUNCTIONS

Let's say we have a function FOO, then we can replace it for testing by establishing a new mocking context and then specifying how the new function should behave (see below in UTILITIES for a more primitive dynamic function rebinding):

> (declaim (notinline foo bar))
> (defun foo () 'foo)
> (defun bar (&rest args)
>   (declare (ignore args))
>   'bar)
> (with-mocks ()
>   (answer (foo 1) 42)
>   (answer foo 23)
>   (values
>    (eql 42 (foo 1))
>    (eql 23 (foo 'bar))))
> => T T

The ANSWER macro has pattern matching (see TRIVIA) integrated. Therefore something like the following will now work as expected:

> (with-mocks ()
>   (answer (foo x) (format T "Hello, ~A!" x))
>   (foo "world"))
> => "Hello, world!"

If you don't like ANSWER as it is, you can still use IF-CALLED directly. Note however that unless UNHANDLED is called, the function always matches and the return value is directly returned again:

> (with-mocks ()
>   (if-called 'foo (lambda (x)
>                     (unhandled)
>                     (error "Not executed!")))
>   (if-called 'foo (lambda (x) (format T "Hello, ~A!" x)))
>   (foo "world"))
> => "Hello, world!"

Be especially careful to handle all given arguments, otherwise the function call will fail and that error is propagated upwards.

IF-CALLED also has another option to push a binding to the front of the list, which (as of now) isn't available via ANSWER (and should be treated as subject to change anyway).

Should you wish to run the previously defined function, use the function CALL-PREVIOUS. If no arguments are passed it will use the current arguments from *ARGUMENTS*, if any. Otherwise it will be called with the passed arguments instead. For cases where explicitely calling it with no arguments is necessary, using (funcall *previous*) is still possible as well.

> (with-mocks ()
>   (answer foo `(was originally ,(funcall *previous*)))
>   (answer bar `(was originally ,(call-previous)))
>   (values
>    (foo "hello")
>    (bar "hello")))
> => (WAS ORIGINALLY FOO) (WAS ORIGINALLY BAR)

The function INVOCATIONS may be used to retrieve all recorded invocations of mocks (so far); the optional argument can be used to filter for a particular name:

> (with-mocks ()
>   (answer foo)
>   (foo "hello")
>   (foo "world")
>   (bar "test")
>   (invocations 'foo))
> => ((FOO "hello")
>     (FOO "world"))

Currently there are no further predicates to check these values, this is however an area of investigation, so presumably either a macro like FIVEAMs IS, or regular predicates could appear in this place.

EXAMPLES

The following examples may give a better impression.

Here we test a particular ECLASTIC method, GET*. In order to replace the HTTP call with a supplied value, we use ANSWER with HTTP-REQUEST and return a pre-filled stream. Afterwards both the number of INVOCATIONS and the actual returned values are checked.

(use-package '(#:cl-mock #:fiveam #:eclastic #:drakma #:puri))

(def-test search.empty ()
  (let* ((events (make-instance '<type> :type "document" :index "index"
                                        :host "localhost" :port 9292))
         (text "{\"took\":3,\"timed_out\":false,\"_shards\":{\"total\":5,\
\"successful\":5,\"failed\":0},\"hits\":{\"total\":123,\"max_score\":1.0,\
\"hits\":[{\"_index\":\"index\",\"_type\":\"document\",\"_id\":\"12345\",\
\"_score\":1.0,\"_source\":{\"test\": \"Hello, World!\"}}]}}")
         (stream (make-string-input-stream text)))
    (with-mocks ()
      (answer http-request
        (values stream 200 NIL
                (parse-uri "http://localhost:9292/index/document/_search")
                stream NIL "OK"))
      (let ((values (multiple-value-list
                     (get* events (new-search NIL)))))
        (is (eql 1 (length (invocations))))
        (is (eql 1 (length (car values))))
        (is-true (typep (caar values) '<document>))
        (is (equal (cdr values)
                   '(NIL (:hits 123
                          :shards (:total 5 :failed 0 :successful 5)
                          :timed-out NIL :took 3))))))))

Of course, running this should produce no errors:

> (run! 'search.empty)
>
> Running test SEARCH.EMPTY ....
> Did 4 checks.
>    Pass: 4 (100%)
>    Skip: 0 ( 0%)
>    Fail: 0 ( 0%)
>
> => NIL

UTILITIES

DFLET dynamically rebinds functions similar to FLET:

> (defun foo () 42)
> (defun bar () (foo))
> (bar)
> => 42
> (dflet ((foo () 23))
>   (bar))
> => 23
> (OR) => 42, if FOO was inlined

The caveat is that this might not work on certain optimisation settings, including inlining. That trade-off seems acceptable; it would be nice if a warning could be issued depending on the current optimisation settings, however that is particularly implementation dependent, so lack of a warning won't indicate a working environment.

The underlying function PROGF may be used as well similarly to the standard PROG:

> (progf '(foo) (list (lambda () 23))
>   (bar))
> => 23
> (OR) => 42, if FOO was inlined

Dependencies (5)

  • alexandria
  • bordeaux-threads
  • closer-mop
  • fiveam
  • trivia
  • GitHub
  • Quicklisp