mockr/ 0000755 0001762 0000144 00000000000 14366454362 011402 5 ustar ligges users mockr/NAMESPACE 0000644 0001762 0000144 00000000166 14203657336 012620 0 ustar ligges users # Generated by roxygen2: do not edit by hand export(get_mock_env) export(local_mock) export(with_mock) import(rlang) mockr/README.md 0000644 0001762 0000144 00000011745 14365473162 012667 0 ustar ligges users # mockr [](https://github.com/krlmlr/mockr/actions) [](https://cran.r-project.org/package=mockr) [](https://app.codecov.io/gh/krlmlr/mockr?branch=main) The goal of mockr is to provide a drop-in replacement for [`testthat::local_mock()`](https://testthat.r-lib.org/reference/with_mock.html) and [`testthat::with_mock()`](https://testthat.r-lib.org/reference/with_mock.html) which is deprecated in testthat 3.0.0. The functions [`mockr::local_mock()`](https://krlmlr.github.io/mockr/reference/local_mock.html) and [`mockr::with_mock()`](https://krlmlr.github.io/mockr/reference/local_mock.html) are modeled closely after the original implementation, but now only allow mocking functions in the package under test. In contrast to the original implementation, no fiddling with R’s internals is needed, and the implementation plays well with byte-compiled code. There are some caveats, though: 1. Mocking external functions (in other packages) doesn’t work anymore. This is by design. - If you need to mock an external function, write a wrapper. - If that external function is called by third-party code, you’ll need to perhaps mock that third-party code, or look for a different way of implementing this test or organizing your code. 2. You cannot refer to functions in your package via `your.package::` or `your.package:::` anymore. - Remove the `your.package:::`, your code and tests should run just fine without that. If you encounter other problems, please [file an issue](https://github.com/krlmlr/mockr/issues). ## Example
library(mockr) access_resource <- function() { message("Trying to access resource...") # For some reason we can't access the resource in our tests. stop("Can't access resource now.") } work_with_resource <- function() { resource <- access_resource() message("Fetched resource: ", resource) invisible(resource) } # Calling this function gives an error work_with_resource() #> Trying to access resource... #> Error in access_resource(): Can't access resource now. local({ # Here, we override the function that raises the error local_mock(access_resource = function() 42) # No error raised work_with_resource() }) #> Fetched resource: 42## Installation Install from CRAN via
install.packages("mockr")------------------------------------------------------------------------ ## Code of Conduct Please note that the mockr project is released with a [Contributor Code of Conduct](https://krlmlr.github.io/mockr/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. mockr/man/ 0000755 0001762 0000144 00000000000 14366006155 012146 5 ustar ligges users mockr/man/local_mock.Rd 0000644 0001762 0000144 00000005721 14366146276 014556 0 ustar ligges users % Generated by roxygen2: do not edit by hand % Please edit documentation in R/with-mock.R \name{local_mock} \alias{local_mock} \alias{with_mock} \title{Mock functions in a package} \usage{ local_mock( ..., .parent = parent.frame(), .env = get_mock_env(.parent), .defer_env = parent.frame() ) with_mock(..., .parent = parent.frame(), .env = get_mock_env(.parent)) } \arguments{ \item{...}{\verb{[any]}\cr Named arguments redefine mocked functions. An unnamed argument containing code in braces (\code{{}}) should be provided to \code{with_mock()}, it will be evaluated after mocking the functions. Use \verb{:=} to mock functions that start with a dot to avoid potential collision with current or future arguments to \code{with_mock()} or \code{local_mock()}. Passing more than one unnamed argument to \code{with_mock()}, or code that is not inside braces, gives a warning.} \item{.parent}{\verb{[environment]}\cr the environment in which to evaluate the expressions, defaults to \code{\link[=parent.frame]{parent.frame()}}. Usually doesn't need to be changed.} \item{.env}{\verb{[environment]}\cr the environment in which to patch the functions, defaults to \code{\link[=topenv]{topenv()}}. Usually doesn't need to be changed.} \item{.defer_env}{\verb{[environment]}\cr Attach exit handlers to this environment. Typically, this should be either the current environment or a parent frame (accessed through \code{\link[=parent.frame]{parent.frame()}}). This argument is passed on as \code{envir} to \code{\link[withr:defer]{withr::defer()}}.} } \value{ \code{local_mock()} returns \code{NULL}, invisibly. \code{with_mock()} returns the result of the last unnamed argument. Visibility is preserved. } \description{ \code{local_mock()} temporarily substitutes implementations of package functions. This is useful for testing code that relies on functions that are slow, have unintended side effects or access resources that may not be available when testing. \code{with_mock()} substitutes, runs code locally, and restores in one go. } \details{ This works by adding a shadow environment as a parent of the environment in which the expressions are evaluated. Everything happens at the R level, but only functions in your own package can be mocked. Otherwise, the implementation is modeled after the original version in the \code{testthat} package, which is now deprecated. } \examples{ some_func <- function() stop("oops") some_other_func <- function() some_func() my_env <- environment() tester_func <- function() { # The default for .env works well most of the time, # unfortunately not in examples local_mock(some_func = function() 42, .env = my_env) some_other_func() } try(some_other_func()) tester_func() tester_func_with <- function() { with_mock( some_func = function() 42, .env = my_env, { some_other_func() } ) } tester_func_with() } \references{ Suraj Gupta (2012): \href{https://blog.thatbuthow.com/how-r-searches-and-finds-stuff/}{How R Searches And Finds Stuff} } mockr/man/get_mock_env.Rd 0000644 0001762 0000144 00000002302 14203657336 015075 0 ustar ligges users % Generated by roxygen2: do not edit by hand % Please edit documentation in R/env.R \name{get_mock_env} \alias{get_mock_env} \title{Get environment for mocking} \usage{ get_mock_env(.parent = parent.frame()) } \arguments{ \item{.parent}{\verb{[environment]}\cr the environment in which to evaluate the expressions, defaults to \code{\link[=parent.frame]{parent.frame()}}. Usually doesn't need to be changed.} } \description{ Called by default from \code{\link[=with_mock]{with_mock()}} to determine the environment where to update mocked functions. This function is exported to help troubleshooting. } \details{ This function works differently depending on \code{\link[testthat:is_testing]{testthat::is_testing()}}. Outside testthat, \code{topenv(.parent)} is returned. This was the default for mockr < 0.1.0 and works for many cases. In testthat, \code{asNamespace("
The mockr package helps testing code that relies on functions that are slow, have unintended side effects or access resources that may not be available when testing. It allows replacing such functions with deterministic mock functions. This article gives an overview and introduces a few techniques.
library(mockr)
Let’s assume a function access_resource()
that accesses
some resource. This works in normal circumstances, but not during tests.
A function work_with_resource()
works with that resource.
How can we test work_with_resource()
without adding too
much logic to the implementation?
<- function() {
access_resource message("Trying to access resource...")
# For some reason we can't access the resource in our tests.
stop("Can't access resource now.")
}
<- function() {
work_with_resource <- access_resource()
resource message("Fetched resource: ", resource)
invisible(resource)
}
In our example, calling the worker function gives an error:
work_with_resource()
#> Trying to access resource...
#> Error in access_resource(): Can't access resource now.
We can use local_mock()
to temporarily replace the
implementation of access_resource()
with one that doesn’t
throw an error:
<- function() {
access_resource_for_test # We return a value that's good enough for testing
# and can be computed quickly:
42
}
local({
# Here, we override the function that raises the error
local_mock(access_resource = access_resource_for_test)
work_with_resource()
})#> Fetched resource: 42
The use of local()
here is required for technical
reasons. This package is most useful in conjunction with testthat, the
remainder of this article will focus on that use case.
We create a package called {mocktest} for demonstration. For this
demo, the package is created in a temporary directory. A real project
will live somewhere in your home directory. The
usethis::create_package()
function sets up a package
project ready for development. The output shows the details of the
package created.
<- usethis::create_package(file.path(tempdir(), "mocktest"))
pkg #> ✔ Creating '/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/Rtmp2PXiXa/mocktest/'
#> ✔ Setting active project to '/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/Rtmp2PXiXa/mocktest'
#> ✔ Creating 'R/'
#> ✔ Writing 'DESCRIPTION'
#> Package: mocktest
#> Title: What the Package Does (One Line, Title Case)
#> Version: 0.0.0.9000
#> Authors@R (parsed):
#> * First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)
#> Description: What the package does (one paragraph).
#> License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
#> license
#> Encoding: UTF-8
#> Roxygen: list(markdown = TRUE)
#> RoxygenNote: 7.2.3
#> ✔ Writing 'NAMESPACE'
#> ✔ Setting active project to '<no active project>'
In an interactive RStudio session, a new window opens. Users of other environments would change the working directory manually. For this demo, we manually set the active project.
::proj_set()
usethis#> ✔ Setting active project to
#> '/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/Rtmp2PXiXa/mocktest'
The infrastructure files and directories that comprise a minimal R package are created:
::dir_tree()
fs#> .
#> ├── DESCRIPTION
#> ├── NAMESPACE
#> └── R
We copy the functions from the previous example (under different names) into the package. Normally we would use a text editor:
cat > R/resource.R <<"EOF"
access_resource_pkg <- function() {
message("Trying to access resource...")
# For some reason we can't access the resource in our tests.
stop("Can't access resource now.")
}
work_with_resource_pkg <- function() {
resource <- access_resource_pkg()
message("Fetched resource: ", resource)
invisible(resource)
}
EOF
Loading the package and calling the function gives the error we have seen before:
::load_all()
pkgload#> ℹ Loading mocktest
work_with_resource_pkg()
#> Trying to access resource...
#> Error in access_resource_pkg(): Can't access resource now.
We create a test that tests work_with_resource_pkg()
,
mocking access_resource_pkg()
. We need to prefix with the
package name, because testthat provides its own
testthat::local_mock()
which is now deprecated.
::use_testthat()
usethis#> ✔ Adding 'testthat' to Suggests field in DESCRIPTION
#> ✔ Setting Config/testthat/edition field in DESCRIPTION to '3'
#> ✔ Creating 'tests/testthat/'
#> ✔ Writing 'tests/testthat.R'
#> • Call `use_test()` to initialize a basic test file and open it for editing.
cat > tests/testthat/test-resource.R <<"EOF"
test_that("Can work with resource", {
mockr::local_mock(access_resource_pkg = function() {
42
})
expect_message(
expect_equal(work_with_resource_pkg(), 42)
)
})
EOF
The test succeeds:
::test_local(reporter = "location")
testthat#>
#> Attaching package: 'testthat'
#> The following objects are masked from 'package:mockr':
#>
#> local_mock, with_mock
#> Start test: Can work with resource
#> 'test-resource.R:6' [success]
#> 'test-resource.R:6' [success]
#> End test: Can work with resource
mockr is aware of testthat and will work even if executing the tests in the current session. This is especially handy if you want to troubleshoot single tests:
test_that("Can work with resource", {
::local_mock(access_resource_pkg = function() {
mockr42
})
expect_message(
expect_equal(work_with_resource_pkg(), 42)
)
})#> Test passed 🥳
mockr can only mock functions in the current package. To substitute implementations of functions in other packages, create wrappers in your package and use these wrappers exclusively.
The example below demonstrates a d6()
function that is
used to get the value of a random die throw. Instead of using
runif()
directly, this function uses
my_runif()
which wraps runif()
.
cat > R/runif.R <<"EOF"
my_runif <- function(...) {
runif(...)
}
d6 <- function() {
trunc(my_runif(1, 0, 6)) + 1
}
EOF
::load_all()
pkgload#> ℹ Loading mocktest
This allows testing the behavior of d6()
:
test_that("d6() works correctly", {
<- c(0.32, 5.4, 5, 2.99)
seq <- function(...) {
my_runif_mock on.exit(seq <<- seq[-1])
1]]
seq[[
}
::local_mock(my_runif = my_runif_mock)
mockr
expect_equal(d6(), 1)
expect_equal(d6(), 6)
expect_equal(d6(), 6)
expect_equal(d6(), 3)
})#> Test passed 😸
mockr cannot substitute implementations of S3 methods. To substitute
methods for a class "foo"
, implement a subclass and add new
methods only for that subclass. The pillar package contains an
example where a class with changed behavior for dim()
and head()
for the sole purpose of testing.