1 Mocks
dxtr edited this page 2026-10-18 12:07:40 +02:00

Mocks

A mock is a global function replaced for a dynamic extent. with-mocks swaps fdefinitions for its body, records every call, and restores them however the body exits. The syntax is flet's; verification is is with three functions, so a failed verification reads like every other failure.

Mocks live in mock.lisp. Because with-mocks replaces global definitions, a test that uses it must be :serial t (see Parallel).

(with-mocks (binding...) body...)

binding ::= name                             ; records calls, accepts anything, returns NIL
          | (name lambda-list stub-body...)   ; a stub; CALL-ORIGINAL is bound inside

A first mock

(deftest (welcome-email :serial t)
  (with-mocks ((fetch-user (id) (make-user :name "Kim" :email "kim@example.com"))
               send-email)
    (notify-user 42)
    (is (called-p 'fetch-user 42))
    (is (called-p 'send-email "kim@example.com" (any 'string)))
    (is (= 1 (call-count 'send-email)))))

fetch-user is a stub that returns a value; send-email is a bare name that records its calls and returns NIL.

Verification

Call Meaning
(calls &optional name) with name, its argument lists oldest first; without, (name . args) for every mock, in call order
(call-count name) how many times name was called
(called-p name pattern...) some recorded call matched pattern; with no pattern, called at all
(call-original args...) inside a stub body, the function the mock replaced
(any &optional type) a matcher: any argument of type
(satisfying predicate) a matcher: any argument the predicate accepts

A pattern element matches an argument when it is a matcher that accepts it, or when the two are equal; the argument count must match.

(is (called-p 'fetch-user 42))                 ; exact
(is (called-p 'fetch-user (any 'integer)))     ; any integer
(is (called-p 'fetch-user (satisfying #'evenp)))
(is (equal '(connect query disconnect) (mapcar #'first (calls))))  ; call order

calls, call-count and called-p signal an error for a name that is not currently mocked, so a typo is loud. After the body exits, the mocks and their records are gone.

Patterns

;; consecutive answers from the call count
(deftest (retries-twice-then-succeeds :serial t)
  (with-mocks ((fetch-url (url)
                 (if (< (call-count 'fetch-url) 3)
                     (error "timed out: ~a" url)
                     "<html>")))
    (is (string= "<html>" (fetch-with-retries "http://x")))
    (is (= 3 (call-count 'fetch-url)))))

;; a spy: record calls but run the real function
(deftest (cache-computes-once :serial t)
  (with-mocks ((hash (&rest args) (apply #'call-original args)))
    (cached-hash "a")
    (cached-hash "a")
    (is (= 1 (call-count 'hash)))))

Nested with-mocks share the outermost call log; an inner mock of a name already mocked shadows the outer one for its extent.

Generic functions and setf

A generic function is replaced wholesale for the extent, and call-original dispatches normally. A (setf foo) function can be mocked too:

(with-mocks ((area (shape) (* 2 (call-original shape))))   ; wrap the generic
  ...)

(with-mocks ((setf configuration))                          ; a setf function
  (setf (configuration) new)
  (is (called-p '(setf configuration) new)))

A mock object for a CLOS protocol is a subclass with defmethods — (defclass fake-db (db) ()) — plus with-mocks on the generic function when calls should be recorded.

What can and cannot be mocked

  • Mocking a name with no global function is an error.
  • Mocking a name in a locked package (e.g. cl:car) is an error, with the advice to call it through a function of your own and mock that.
  • A warning, not an error, if the name has a compiler macro or is declaimed inline: some callers may bypass the mock.

From Mockito

Mockito assay
thenReturn / thenThrow the stub body
consecutive answers a closure over call-count
InOrder, verifyNoMoreInteractions (calls) compared with equal
ArgumentCaptor (calls 'name)
spy a stub that applies call-original
a mock object a subclass with defmethods

Next: Output Format.