Mocking

library(testthat)
library(mockery)

When creating unit tests we want to test each individual unit of code - but say we have fn() function a which depends on a() and b().

a <- function(x) {
  x * 2
}

b <- function(y) {
  y ** 3
}

fn <- function(x, y) {
  a(x) + b(y)
}

Now, we should define tests for all these functions. Let’s start with a() and b(). The tests for these functions aren’t too difficult, the functions have no dependencies so we can just write some simple expectations.

test_that("a doubles the number", {
  expect_equal(a(2), 4)
  expect_equal(a(3), 6)
})
## Test passed πŸ₯‡
test_that("b raises the number to the power 3", {
  expect_equal(b(2), 8)
  expect_equal(b(3), 27)
})
## Test passed πŸ₯³

We could also test fn() by writing something like this:

test_that("fn works as expected", {
  expect_equal(fn(2, 3), 31)
  expect_equal(fn(3, 2), 14)
})
## Test passed πŸ₯‡

But what if our function b was incorrectly defined?

b <- function(y) {
  y * 2 + 1
}

Now, if we re-ran our tests

test_that("b raises the number to the power 3", {
  expect_equal(b(2), 8)
  expect_equal(b(3), 27)
})
## ── Failure (<text>:2:3): b raises the number to the power 3 ────────────────────
## b(2) (`actual`) not equal to 8 (`expected`).
## 
##   `actual`: 5
## `expected`: 8
## 
## ── Failure (<text>:3:3): b raises the number to the power 3 ────────────────────
## b(3) (`actual`) not equal to 27 (`expected`).
## 
##   `actual`:  7
## `expected`: 27

As expected, our tests fail. Great! This helps us identify errors in our code. But what about running the test for fn()?

test_that("fn works as expected", {
  expect_equal(fn(2, 3), 31)
  expect_equal(fn(3, 2), 14)
})
## ── Failure (<text>:2:3): fn works as expected ──────────────────────────────────
## fn(2, 3) (`actual`) not equal to 31 (`expected`).
## 
##   `actual`: 11
## `expected`: 31
## 
## ── Failure (<text>:3:3): fn works as expected ──────────────────────────────────
## fn(3, 2) (`actual`) not equal to 14 (`expected`).
## 
##   `actual`: 11
## `expected`: 14

This test also fails as we depend on b() working correctly. But is fn() behaving incorrectly? We want fn() to call a(x) and add the results to b(y). For this test, should it matter that a() and b() work correctly?

This is where mocking comes in. What we can do is test fn() by replacing the a() and b() functions with some known values that we can easily test.

First, let’s write a test where we simply stub a() and b() with some fixed value. Stubbing intercepts the call to each function and simply returns the result given. In this case, inside of the fn() function we β€œstub” a() with the value 1, and we β€œstub” b() with 2.

test_that("fn works as expected", {
  stub(fn, "a", 1)
  stub(fn, "b", 2)
  
  expect_equal(fn(2, 3), 3)
  expect_equal(fn(3, 2), 3)
})
## Test passed 😸

We can see that this test will then always cause fn() to return 3, no matter the inputs.

A better option may be to stub a() and b() with a function, e.g.Β indentity(), let’s have a quick look at the definition of identity() to see why this is a good fit for stubbing:

identity
## function (x) 
## x
## <bytecode: 0x7f8241247fb8>
## <environment: namespace:base>

this function simple returns it’s input, so let’s see we can use it in a test:

test_that("fn works as expected", {
  stub(fn, "a", identity)
  stub(fn, "b", identity)
  
  expect_equal(fn(2, 3), 2 + 3)
  expect_equal(fn(3, 2), 3 + 2)
})
## Test passed πŸ₯‡

So far we have seen that stub() can return a function call with either a value, or another function. The final thing we can use with stub is a mock().

A mock is an object which is callable (like a function), but it records each call, and the arguments, to that function. This allows us to write tests that check that we are calling the dependencies correctly, but returns fixed known values.

test_that("fn works as expected", {
  # create the mocks: we give values for the 1st call, then the 2nd call. If we try to call fn() a third time we will
  # get an error because we haven't defined a third mock call.
  ma <- mock(1, 2)
  mb <- mock(3, 4)
  # stub fn with our mocks
  stub(fn, "a", ma)
  stub(fn, "b", mb)
  
  # run the function and check the results are what we would expect given the "mocked" results
  expect_equal(fn(2, 3), 1 + 3)
  expect_equal(fn(3, 2), 2 + 4)
  
  # a was called twice
  expect_called(ma, 2)
  # the calls to a each time were a(x)
  expect_call(ma, 1, a(x))
  expect_call(ma, 2, a(x))
  # but the arguments were different
  expect_args(ma, 1, 2)
  expect_args(ma, 2, 3)
  
  # b was called twice
  expect_called(mb, 2)
  # the calls to a each time were a(x)
  expect_call(mb, 1, b(y))
  expect_call(mb, 2, b(y))
  # but the arguments were different
  expect_args(mb, 1, 3)
  expect_args(mb, 2, 2)
})
## Test passed πŸ₯‡

We’ve now built a test for fn() that does not depend on a() or b() working correctly (our tests for b() still fails, but our tests for fn() work).