Mocking testthat
When making a function that gets data from a REST API I use the following pattern.
If X
is a descriptive name for the data source I make a function get_X
, which is intended for users to get the data.
The get_X
is split into two parts:
One for getting the HTTP response (download_X
) and one for parsing this response into an R object (parse_X
):
get_X <- function(...) {
response <- download_X(...)
parse_X(response)
}
The reason for splitting get_X
is to separate the responsibilities as download_X
and parse_X
are very different – download_X
relies on external resources and parse_X
is local data wrangling.
The two function should therefore also be tested independently.
The httr vignette Best practices for API packages is a good guide on how to make a download_X
.
Testing download
Relevant tests for download_X
could be that the the response succeeded and the content type is as expected:
test_that("Download X", {
response <- download_X(...)
expect_equal(httr::status_code(response), 200)
expect_equal(httr::content_type(response), "application/json")
})
In some cases it could also be relevant to test that the URL of the response is as expected.
When testing parse_X
I use a reference response.
I try to find a suitable response, that is, one that is representative and small.
This response is saved in the subfolder testdata
of the inst folder and I include a small function in the package to retrieve it:
testdata_file <- function(filename) {
system.file(file.path("testdata", filename), package = "mypackage", mustWork = TRUE)
}
It may also be relevant to check that the content of response
has not changed:
reference_response <- readRDS(testdata_file("X_response.rds"))
expect_equal(
httr::content(response, "text")
httr::content(reference_response, "text")
)
Testing parse
The function parse_X
use the saved response as a starting point.
test_that("Parse X", {
response <- readRDS(testdata_file("X_response.rds"))
result <- parse_X(result)
})
The nature of result
depends on the situation and the tests of course depends on what parse_X
returns.
If result
is a tibble (which I highly prefer over a plain data frame) I test that it is indeed a tibble, that it has the right column names, that the columns have the right types and that it contains data.
expect_s3_class(result, "tbl")
expect_gt(nrow(result), 0)
expect_named(result, c("col1", "col2"))
column_types <- vapply(result, function(x) class(x)[1], character(1))
expect_equal(
column_types,
c("col1" = <col1's type>,
"col2" = <col2's type>)
)
The vector column_types
contains the first element in the class
vector for each column.
My main usecase is that I only want to check that date-time columns are POSIXct
Testing get
Finally it is get_X
's turn.
With a small response parse_X
should execute fast, but download_X
is most likely much slower and more unpredictable.
Therefore, I want to mock download_X
when testing get_X
such that it just returns the saved response.
The testthat
package offers a mocking functionality:
test_that("Get X", {
local_mock(
download_X = readRDS(testdata_file("X_response.rds"))
)
result <- get_X(...)
})
The local_mock
(or with_mock
) from testthat
command work as intended with devtools::test()
, but not with the covr package:
> covr::package_coverage()
Error: Failure in `path/to/testthat.Rout.fail`
{
stop("Can't mock functions in base packages (", pkg_name, ")", call. = FALSE)
}
name <- gsub(pkg_and_name_rx, "\\2", qual_name)
if (pkg_name == "") {
pkg_name <- .env
}
env <- asNamespace(pkg_name)
if (!exists(name, envir = env, mode = "function")) {
stop("Function ", name, " not found in environment ", environmentName(env), ".", call. = FALSE)
}
mock(name = name, env = env, new = funs[[qual_name]])
})
4: FUN(X[[i]], ...)
5: stop("Function ", name, " not found in environment ", environmentName(env), ".", call. = FALSE)
Error: testthat unit tests failed
Fortunately, the mockery package works with both testthat
and covr
.
It also allows more fine-grained mocking.
test_that("Get X", {
mockery::stub(
get_X,
"download_X",
readRDS(testdata_file("X_response.rds"))
)
result <- get_X(...)
})
Executing tests
Say the “Download X” test is located in the file tests/testthat/test-API.R
.
To run only the tests in test-API.R
use the test command with a filter:
devtools::test(filter = "API")
The filter argument relies on grepl
to choose test files, so to run all tests, except those in test-API.R
we can supply an extra argument:
devtools::test(filter = "API", invert = TRUE)