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).