Skip to content

Commit 3cb0f41

Browse files
authored
Mock in more places (#1748)
Since the package environment is locked, we can only change bindings there, not add them. But that's ok because if we're trying to mock something that's not in the package, it must be imported from another namespace.
1 parent 3ddcb19 commit 3cb0f41

File tree

7 files changed

+121
-36
lines changed

7 files changed

+121
-36
lines changed

NAMESPACE

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ S3method(format,expectation_success)
1515
S3method(format,mismatch_character)
1616
S3method(format,mismatch_numeric)
1717
S3method(is_informative_error,default)
18-
S3method(mockable_generic,integer)
1918
S3method(output_replay,character)
2019
S3method(output_replay,error)
2120
S3method(output_replay,message)
@@ -30,6 +29,7 @@ S3method(print,testthat_results)
3029
S3method(snapshot_replay,character)
3130
S3method(snapshot_replay,condition)
3231
S3method(snapshot_replay,source)
32+
S3method(test_mock_method,integer)
3333
S3method(testthat_print,default)
3434
export("%>%")
3535
export(CheckReporter)

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# testthat (development version)
22

3+
* `with_mocked_bindings()` and `local_mocked_bindings()` can now bind in the
4+
imports namespace too.
5+
36
# testthat 3.1.7
47

58
* `expect_setequal()` gives more actionable feedback (#1657).

R/mock.R

-5
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,3 @@ reset_mock <- function(mock) {
151151
is_base_pkg <- function(x) {
152152
x %in% rownames(utils::installed.packages(priority = "base"))
153153
}
154-
155-
test_mock1 <- function() {
156-
test_mock2()
157-
}
158-
test_mock2 <- function() 10

R/mock2.R

+55-10
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,21 @@ local_mocked_bindings <- function(..., .package = NULL, .env = caller_env()) {
3030
.package <- .package %||% dev_package()
3131
ns_env <- ns_env(.package)
3232

33-
# Unlock bindings and set values
34-
nms <- names(bindings)
35-
locked <- env_binding_unlock(ns_env, nms)
36-
withr::defer(env_binding_lock(ns_env, nms[locked]), envir = .env)
37-
local_bindings(!!!bindings, .env = ns_env, .frame = .env)
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))
36+
bindings_found <- rep_named(names(bindings), FALSE)
37+
for (env in envs) {
38+
this_bindings <- env_has(env, names(bindings)) & !bindings_found
39+
40+
local_bindings_unlock(!!!bindings[this_bindings], .env = env, .frame = .env)
41+
bindings_found <- bindings_found | this_bindings
42+
}
43+
44+
if (any(!bindings_found)) {
45+
missing <- names(bindings)[!bindings_found]
46+
cli::cli_abort("Can't find binding for {.arg {missing}}")
47+
}
3848

3949
invisible()
4050
}
@@ -48,6 +58,23 @@ with_mocked_bindings <- function(code, ..., .package = NULL) {
4858

4959
# helpers -----------------------------------------------------------------
5060

61+
# Wrapper around local_bindings() that automatically unlocks and takes
62+
# list of bindings.
63+
local_bindings_unlock <- function(..., .env = .frame, .frame = caller_env()) {
64+
bindings <- list2(...)
65+
if (length(bindings) == 0) {
66+
return()
67+
}
68+
69+
nms <- names(bindings)
70+
locked <- env_binding_unlock(.env, nms)
71+
withr::defer(env_binding_lock(.env, nms[locked]), envir = .frame)
72+
73+
local_bindings(!!!bindings, .env = .env, .frame = .frame)
74+
75+
invisible()
76+
}
77+
5178
dev_package <- function() {
5279
if (is_testing() && testing_package() != "") {
5380
testing_package()
@@ -81,11 +108,29 @@ check_bindings <- function(x, error_call = caller_env()) {
81108
}
82109
}
83110

84-
# In package
85-
mockable_generic <- function(x) {
86-
UseMethod("mockable_generic")
111+
# For testing -------------------------------------------------------------
112+
113+
test_mock_package <- function() {
114+
test_mock_package2()
115+
}
116+
test_mock_package2 <- function() "y"
117+
118+
test_mock_base <- function() {
119+
identity("y")
120+
}
121+
122+
test_mock_imports <- function() {
123+
as.character(sym("x"))
124+
}
125+
126+
test_mock_namespaced <- function() {
127+
as.character(rlang::sym("x"))
128+
}
129+
130+
test_mock_method <- function(x) {
131+
UseMethod("test_mock_method")
87132
}
88133
#' @export
89-
mockable_generic.integer <- function(x) {
90-
1
134+
test_mock_method.integer <- function(x) {
135+
"y"
91136
}

tests/testthat/_snaps/mock2.md

+8
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@
1111
Error in `local_mocked_bindings()`:
1212
! All elements of `...` must be functions.
1313

14+
# can't mock bindings that don't exist
15+
16+
Code
17+
local_mocked_bindings(f = function() "x")
18+
Condition
19+
Error in `local_mocked_bindings()`:
20+
! Can't find binding for `f`
21+

tests/testthat/test-mock.R

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ test_that("can change value of internal function", {
77
local_edition(2)
88

99
with_mock(
10-
test_mock2 = function() 5,
11-
expect_equal(test_mock1(), 5)
10+
test_mock_package2 = function() 5,
11+
expect_equal(test_mock_package(), 5)
1212
)
1313

1414
# and value is restored on error
1515
expect_error(
1616
with_mock(
17-
test_mock2 = function() 5,
17+
test_mock_package2 = function() 5,
1818
stop("!")
1919
)
2020
)
21-
expect_equal(test_mock1(), 10)
21+
expect_equal(test_mock_package(), "y")
2222
})
2323

2424

@@ -27,8 +27,8 @@ test_that("mocks can access local variables", {
2727
x <- 5
2828

2929
with_mock(
30-
test_mock2 = function() x,
31-
expect_equal(test_mock1(), 5)
30+
test_mock_package2 = function() x,
31+
expect_equal(test_mock_package(), 5)
3232
)
3333
})
3434

tests/testthat/test-mock2.R

+48-14
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
11
test_that("with_mocked_bindings affects local bindings", {
2-
out <- with_mocked_bindings(test_mock1(), test_mock2 = function() "x")
2+
out <- with_mocked_bindings(
3+
test_mock_package(),
4+
test_mock_package2 = function() "x"
5+
)
36
expect_equal(out, "x")
4-
5-
expect_equal(test_mock1(), 10)
7+
expect_equal(test_mock_package(), "y")
68
})
79

810
test_that("local_mocked_bindings affects local bindings", {
911
local({
10-
local_mocked_bindings(test_mock2 = function() "x")
11-
expect_equal(test_mock1(), "x")
12+
local_mocked_bindings(test_mock_package2 = function() "x")
13+
expect_equal(test_mock_package(), "x")
1214
})
1315

14-
expect_equal(test_mock1(), 10)
16+
expect_equal(test_mock_package(), "y")
1517
})
1618

17-
test_that("local_mocked_bindings affects S3 methods", {
18-
skip("currently fails")
19+
test_that("unlocks and relocks binding if needed", {
20+
expect_true(env_binding_are_locked(base_env(), "identity"))
1921

2022
local({
21-
local_mocked_bindings(mockable_generic.integer = function(x) 2)
22-
expect_equal(mockable_generic(1L), 2)
23+
local_mocked_bindings(identity = function(...) "x")
24+
expect_false(env_binding_are_locked(base_env(), "identity"))
2325
})
24-
expect_equal(mockable_generic(1L), 1)
26+
27+
expect_true(env_binding_are_locked(base_env(), "identity"))
2528
})
2629

2730
test_that("can make wrapper", {
2831
local_mock_x <- function(env = caller_env()) {
29-
local_mocked_bindings(test_mock2 = function() "x", .env = env)
32+
local_mocked_bindings(test_mock_package2 = function() "x", .env = env)
3033
}
3134

3235
local({
3336
local_mock_x()
34-
expect_equal(test_mock1(), "x")
37+
expect_equal(test_mock_package(), "x")
3538
})
3639

37-
expect_equal(test_mock1(), 10)
40+
expect_equal(test_mock_package(), "y")
3841
})
3942

4043
test_that("with_mocked_bindings() validates its inputs", {
@@ -43,3 +46,34 @@ test_that("with_mocked_bindings() validates its inputs", {
4346
with_mocked_bindings(1 + 1, x = 2)
4447
})
4548
})
49+
50+
# -------------------------------------------------------------------------
51+
52+
test_that("can mock bindings from imports", {
53+
local_mocked_bindings(readLines = function(...) "x")
54+
expect_equal(test_mock_imports(), "x")
55+
})
56+
57+
test_that("can mock bindings from base", {
58+
local_mocked_bindings(identity = function(...) "x")
59+
expect_equal(test_mock_base(), "x")
60+
})
61+
62+
test_that("can mock binding in another package", {
63+
local_mocked_bindings(sym = function(...) "x", .package = "rlang")
64+
expect_equal(test_mock_namespaced(), "x")
65+
})
66+
67+
test_that("can mock S3 methods", {
68+
skip("currently fails")
69+
70+
local({
71+
local_mocked_bindings(test_mock_method.integer = function(...) "x")
72+
expect_equal(test_mock_method(1L), "x")
73+
})
74+
expect_equal(test_mock_method(1L), "y")
75+
})
76+
77+
test_that("can't mock bindings that don't exist", {
78+
expect_snapshot(local_mocked_bindings(f = function() "x"), error = TRUE)
79+
})

0 commit comments

Comments
 (0)