Ch4 Mocking
The term “mocking” refers to producing a “fake” function or component to use only during testing. The origin of the term is the same as the word “mock-up”, which is a non-functional product made for testing look or feel, or for demonstrating the appearance. Usually the function being replaced is complex, whereas the “mock” function is simplified or only made to work for certain cases.
Simplified example: suppose (sum-first-N #'func 100)
applies a given
function to the first 100 integers and adds the results.
This function could have a mock
version that is only correct when func is the identity function.
(defun mock-sum-first-N (f n)
(loop for x from 1 to n
sum x))
Another possibility is that a mock function could be passed that
only works when N<5 (this is pointless but could be used to detect
mistakes in sum-first-N
function).
(defun mock-func (num)
(if (< num 5)
(* num num)
(error "~A is too large of number" num)))
(sum-first-N #'mock-func 100)
Why Mock?
We are learning about using mock functions for testing because the
achieve-all
and achieve-one
form a mutually recursive system that
does not let you test each function individually. When we add an input
to the achieve-all
function telling it which function to call in
order to achieve a single goal (achieve-one
), we can pass in a
version of the function that we know gives the right answer under
certain controlled circumstances. This lets us make sure the
achieve-all
function works in isolation before coupling it with the
achieve-one
function. Of course we perform similar testing with the
achieve-one
function in isolation as well.
Example of Mocking
This example is courtesy of John Wu.
Here’s a simpler example of this that came up earlier for people who are still confused about what mocking is.
There was a post about how we can
check functions that have a (random x)
in them because well it’s
random so there should be no feasible way to check it. The solution
uses something very similar to mocking, and with a little adjustment,
it can actually serve as a good example of mocking.
Okay, so back from the beginning let use the example given there. So
let’s say that we want to recreate the random-pick-word
function from
the example earlier.
(defun random-pick-word ()
"Picks a random word from 'go', 'fish', or 'fry'"
(elt '("go" "fish" "fry")
(random 3)))
One way we can quickly rewrite this to replicate how achieve-all
and
achieve-one
works on at least a surface level is to write it like
this. In other words, we can pass a parameter to the element that
allows us to mock the hard to test parts. In this case that would be
the random part for reasons given earlier.
(defun random-pick-word (&key (mocked-part (random 3)))
"random-pick-word but modified to take in which elem it picks, defaults to random"
(elt '("go" "fish" "fry") mocked-part))
Now by passing the function simpler elements to test, we can actually
test to see if the random-pick-word
works as we intend it to for the
possible values we can pass it.
(define-test test-random-pick-word
(assert-equal "go" (random-pick-word :mocked-part 0))
(assert-equal "fish" (random-pick-word :mocked-part 1))
(assert-equal "fry" (random-pick-word :mocked-part 2)))
Now assumedly when all of these tests pass, we know that our function
will work with the random component because every possible thing that
(random x)
could pass it has been tested to work. We have successfully
mock-tested our random function.
Advanced note
Super-technical note: if you actually try this, you will see the
random choice does not change with different calls to
(random-pick-word)
because the default value is chosen by evaluating
(random 3)
only once when the function is defined. That’s too
technical to worry about right now.