With version 3.0.0 of testthat
, mocking capabilities
provided by testthat::with_mock()
and testthat::local_mock()
have been deprecated under edition 3. This leaves implementation of
function mocking for unit testing to third-party packages, of which two
have been published on CRAN: mockery
and mockr
.
While all currently available mocking implementations have their
limitations, what sets mockthat
apart from
mockery
and mockr
is coping with S3 dispatch
(see example below).
You can install the development version of mockthat
from
GitHub with:
# install.packages("devtools")
::install_github("nbenn/mockthat") devtools
A release version will be submitted to CRAN shortly.
Mocking in the context of unit testing refers to temporarily replacing a piece of functionality (that might be part of the package being tested or potentially even part of a downstream dependency) in order to cope with limited infrastructure in testing environments (for example absence of a live Internet connection).
For a function download_data()
implemented as
<- function(url) {
download_data ::curl_fetch_memory(url)
curl }
we do not want to have to rely on a live internet connection for
writing a unit test. With help of mockthat
, we can
substitute curl::curl_fetch_memory()
with a stub that simply returns a constant.
library(mockthat)
<- "https://eu.httpbin.org/get?foo=123"
url
with_mock(
`curl::curl_fetch_memory` = function(...) '["mocked request"]',
download_data(url)
)#> [1] "[\"mocked request\"]"
As mentioned above, the main point of differentiation of
mockthat
over the other available packages
mockery
and mockr
is stubbing out functions in
the context of S3 dispatch. Assuming the following set-up,
<- function(x) UseMethod("gen")
gen
<- function(x) foo(x)
met
<- function(x) stop("foo")
foo
.S3method("gen", "cls", met)
<- structure(123, class = "cls")
x
gen(x)
#> Error in foo(x): foo
mockthat::with_mock()
can be used to catch the call to
foo()
and therefore prevent the error from being
thrown.
::with_mock(
mockthatfoo = function(x) "bar",
met(x)
)#> [1] "bar"
This is not possible with the current implementation of mockr::with_mock()
.
::with_mock(
mockrfoo = function(x) "bar",
met(x)
)#> Warning: Replacing functions in evaluation environment: `foo()`
#> Warning: The code passed to `with_mock()` must be a braced expression to get
#> accurate file-line information for failures.
#> [1] "bar"
And with the current API of mockery::stub()
it is unclear how the depth
argument should be chosen, as
the function gen()
does not contain a call to
met()
. Trying a range of sensible values does not yield the
desired result.
for (depth in seq_len(3L)) {
::stub(gen, "foo", "bar", depth = depth)
mockerytryCatch(met(x), error = function(e) message("depth ", depth, ": nope"))
}#> depth 1: nope
#> depth 2: nope
#> depth 3: nope
Borrowing from mockery
, mockthat
also
allows for creating mock objects (with class attribute
mock_fun
), which allow capture of the call for later
examination.
<- mock("mocked request")
mk <- function(url) curl::curl(url)
dl
with_mock(`curl::curl` = mk, dl(url))
#> [1] "mocked request"
mock_call(mk)
#> curl::curl(url = url)
mock_args(mk)
#> $url
#> [1] "https://eu.httpbin.org/get?foo=123"
#>
#> $open
#> [1] ""
#>
#> $handle
#> <curl handle> (empty)
In addition to with_mock()
, mockthat
also
offers a local_mock()
function, again, mimicking the
deprecated testthat
function, which keeps the mocks in
place for the life-time of the environment passed as
local_env
argument (or if called from the global
environment, until withr::deferred_run()
is executed). Mock
objects as shown above are created (and returned invisibly) for all
non-function objects passed as ...
.
<- new.env()
tmp <- local_mock(`curl::curl` = "mocked request", local_env = tmp)
mk dl(url)
#> [1] "mocked request"
mock_arg(mk, "url")
#> [1] "https://eu.httpbin.org/get?foo=123"