33# ' @description
44# ' `r lifecycle::badge("experimental")`
55# '
6+ # ' `with_mocked_bindings()` and `local_mocked_bindings()` provide tools for
7+ # ' "mocking", temporarily redefining a function so that it behaves differently
8+ # ' during tests. This is helpful for testing functions that depend on external
9+ # ' state (i.e. reading a value from a file or a website, or pretending a package
10+ # ' is or isn't installed).
11+ # '
612# ' These functions represent a second attempt at bringing mocking to testthat,
7- # ' incorporating what we've learned from the mockr, mockery, and mockthat package.
13+ # ' incorporating what we've learned from the mockr, mockery, and mockthat
14+ # ' packages.
15+ # '
16+ # ' # Use
17+ # '
18+ # ' There are four places that the function you are trying to mock might
19+ # ' come from:
20+ # '
21+ # ' * Internal to your package.
22+ # ' * Imported from an external package via the `NAMESPACE`.
23+ # ' * The base environment.
24+ # ' * Called from an external package with `::`.
25+ # '
26+ # ' They are described in turn below.
27+ # '
28+ # ' ## Internal & imported functions
29+ # '
30+ # ' You mock internal and imported functions the same way. For example, take
31+ # ' this code:
32+ # '
33+ # ' ```R
34+ # ' some_function <- function() {
35+ # ' another_function()
36+ # ' }
37+ # ' ```
38+ # '
39+ # ' It doesn't matter whether `another_function()` is defined by your package
40+ # ' or you've imported it from a dependency with `@import` or `@importFrom`,
41+ # ' you mock it the same way:
42+ # '
43+ # ' ```R
44+ # ' local_mocked_bindings(
45+ # ' another_function = function(...) "new_value"
46+ # ' )
47+ # ' ```
48+ # '
49+ # ' ## Base functions
50+ # '
51+ # ' Note that it's not possible to mock functions in the base namespace
52+ # ' (i.e. functions that you can use without explicitly importing them)
53+ # ' since currently we don't know of a way to to mock them without potentially
54+ # ' affecting all running code. If you need to mock a base function, you'll
55+ # ' need to create a wrapper, as described below.
56+ # '
57+ # ' ## Namespaced calls
58+ # '
59+ # ' It's trickier to mock functions in other packages that you call with `::`.
60+ # ' For example, take this minor variation:
861# '
9- # ' `with_mocked_bindings()` and `local_mocked_bindings()` work by temporarily
10- # ' changing variable bindings in the namespace of namespace `.package`.
11- # ' Generally, it's only safe to mock packages that you own. If you mock other
12- # ' packages, we recommend using `skip_on_cran()` to avoid CRAN failures if the
13- # ' implementation changes.
62+ # ' ```R
63+ # ' some_function <- function() {
64+ # ' anotherpackage::another_function()
65+ # ' }
66+ # ' ```
1467# '
15- # ' These functions do not currently affect registered S3 methods.
68+ # ' To mock here, you'll need to modify `another_function()` inside the
69+ # ' `anotherpackage` package. You _can_ do this by supplying the `.package`
70+ # ' argument:
1671# '
72+ # ' ```R
73+ # ' local_mocked_bindings(
74+ # ' another_function = function(...) "new_value",
75+ # ' .package = "anotherpackage"
76+ # ' )
77+ # ' ```
78+ # '
79+ # ' But it's not a great idea to mock a namespace that you don't own because
80+ # ' it affects all code in that package, not just code in your package. Instead,
81+ # ' it's safer to either import the function into your package, or make a wrapper
82+ # ' that you can mock:
83+ # '
84+ # ' ```R
85+ # ' some_function <- function() {
86+ # ' my_wrapper()
87+ # ' }
88+ # ' my_wrapper <- function(...) {
89+ # ' anotherpackage::another_function(...)
90+ # ' }
91+ # '
92+ # ' local_mocked_bindings(
93+ # ' my_wrapper = function(...) "new_value"
94+ # ' )
95+ # ' ```
1796# ' @export
1897# ' @param ... Name-value pairs providing functions to mock.
1998# ' @param code Code to execute with specified bindings.
@@ -30,15 +109,26 @@ local_mocked_bindings <- function(..., .package = NULL, .env = caller_env()) {
30109 .package <- .package %|| % dev_package()
31110 ns_env <- ns_env(.package )
32111
33- # Rebind, first looking in package namespace, then imports, then the base
34- # namespace, then the global environment
35- envs <- c(list (ns_env ), env_parents(ns_env ))
112+ # Rebind in namespace, imports, and the global environment
113+ envs <- list (ns_env , env_parent(ns_env ), globalenv())
36114 bindings_found <- rep_named(names(bindings ), FALSE )
37115 for (env in envs ) {
38- this_bindings <- env_has(env , names(bindings )) & ! bindings_found
116+ local_bindings_rebind(!!! bindings , .env = env , .frame = .env )
117+ bindings_found <- bindings_found | env_has(env , names(bindings ))
118+ }
39119
40- local_bindings_unlock(!!! bindings [this_bindings ], .env = env , .frame = .env )
41- bindings_found <- bindings_found | this_bindings
120+ # And mock S3 methods
121+ methods_env <- ns_env [[" .__S3MethodsTable__." ]]
122+ local_bindings_rebind(!!! bindings , .env = methods_env , .frame = .env )
123+
124+ # If needed, also mock in the package environment so we can call directly
125+ if (is_attached(paste0(" package:" , .package ))) {
126+ local_bindings_rebind(!!! bindings , .env = pkg_env(.package ), .frame = .env )
127+ }
128+ # And in the current testing environment
129+ test_env <- testthat_env $ current_test_env
130+ if (! is.null(test_env )) {
131+ local_bindings_rebind(!!! bindings , .env = test_env , .frame = .env )
42132 }
43133
44134 if (any(! bindings_found )) {
@@ -58,10 +148,13 @@ with_mocked_bindings <- function(code, ..., .package = NULL) {
58148
59149# helpers -----------------------------------------------------------------
60150
61- # Wrapper around local_bindings() that automatically unlocks and takes
62- # list of bindings.
63- local_bindings_unlock <- function (... , .env = .frame , .frame = caller_env()) {
151+ # Wrapper around local_bindings() that only rebinds existing values,
152+ # automatically unlocking as needed. We can only rebind because most of
153+ # these environments are locked, meaning we can't add new bindings.
154+ local_bindings_rebind <- function (... , .env = .frame , .frame = caller_env()) {
64155 bindings <- list2(... )
156+ bindings <- bindings [env_has(.env , names(bindings ))]
157+
65158 if (length(bindings ) == 0 ) {
66159 return ()
67160 }
@@ -110,21 +203,21 @@ check_bindings <- function(x, error_call = caller_env()) {
110203
111204# For testing -------------------------------------------------------------
112205
113- test_mock_package <- function () {
114- test_mock_package2()
206+ test_mock_direct <- function () {
207+ " y "
115208}
116- test_mock_package2 <- function () " y"
117209
118- test_mock_base <- function () {
119- identity( " y " )
210+ test_mock_internal <- function () {
211+ test_mock_internal2( )
120212}
213+ test_mock_internal2 <- function () " y"
121214
122215test_mock_imports <- function () {
123- as.character(sym(" x " ))
216+ as.character(sym(" y " ))
124217}
125218
126219test_mock_namespaced <- function () {
127- as.character(rlang :: sym(" x " ))
220+ as.character(rlang :: sym(" y " ))
128221}
129222
130223test_mock_method <- function (x ) {
@@ -134,3 +227,15 @@ test_mock_method <- function(x) {
134227test_mock_method.integer <- function (x ) {
135228 " y"
136229}
230+
231+
232+ show_bindings <- function (name , env = caller_env()) {
233+ envs <- env_parents(env )
234+ has_binding <- Filter(function (env ) env_has(env , name ), envs )
235+ lapply(has_binding , env_desc )
236+ invisible ()
237+ }
238+
239+ env_desc <- function (env ) {
240+ cat(obj_address(env ), " : " , env_name(env ), " \n " , sep = " " )
241+ }
0 commit comments