Skip to content

Commit ec80bef

Browse files
committed
Merged origin/main into gh-download
2 parents 62bf480 + c0c1fff commit ec80bef

File tree

10 files changed

+347
-2
lines changed

10 files changed

+347
-2
lines changed

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ export(it)
158158
export(local_edition)
159159
export(local_mock)
160160
export(local_mocked_bindings)
161+
export(local_mocked_r6_class)
162+
export(local_mocked_s3_method)
163+
export(local_mocked_s4_method)
161164
export(local_on_cran)
162165
export(local_reproducible_output)
163166
export(local_snapshotter)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# testthat (development version)
22

33
* New `snapshot_download_gh()` makes it easy to get snapshots off GitHub and into your local package (#1779).
4+
* New `local_mocked_s3_method()`, `local_mocked_s4_method()`, and `local_mocked_r6_class()` allow you to mock S3 and S4 methods and R6 classes (#1892, #1916)
45
* `expect_snapshot_file(name=)` must have a unique file path. If a snapshot file attempts to be saved with a duplicate `name`, an error will be thrown. (#1592)
56
* `test_dir()`, `test_file()`, `test_package()`, `test_check()`, `test_local()`, `source_file()` gain a `shuffle` argument uses `sample()` to randomly reorder the top-level expressions in each test file (#1942). This random reordering surfaces dependencies between tests and code outside of any test, as well as dependencies between tests. This helps you find and eliminate unintentional dependencies.
67
* `snapshot_accept(test)` now works when the test file name contains `.` (#1669).

R/mock-oo.R

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#' Mock S3 and S4 methods
2+
#'
3+
#' These functions allow you to temporarily override S3 and S4 methods that
4+
#' already exist. It works by using [registerS3method()]/[setMethod()] to
5+
#' temporarily replace the original definition.
6+
#'
7+
#' @param generic A string giving the name of the generic.
8+
#' @param signature A character vector giving the signature of the method.
9+
#' @param definition A function providing the method definition.
10+
#' @param frame Calling frame which determines the scope of the mock.
11+
#' Only needed when wrapping in another local helper.
12+
#' @export
13+
#' @examples
14+
#' x <- as.POSIXlt(Sys.time())
15+
#' local({
16+
#' local_mocked_s3_method("length", "POSIXlt", function(x) 42)
17+
#' length(x)
18+
#' })
19+
#'
20+
#' length(x)
21+
local_mocked_s3_method <- function(
22+
generic,
23+
signature,
24+
definition,
25+
frame = caller_env()
26+
) {
27+
check_string(generic)
28+
check_string(signature)
29+
check_function(definition)
30+
31+
old <- getS3method(generic, signature, optional = TRUE)
32+
if (is.null(old)) {
33+
cli::cli_abort(
34+
"Can't find existing S3 method {.code {generic}.{signature}()}."
35+
)
36+
}
37+
registerS3method(generic, signature, definition, envir = frame)
38+
withr::defer(registerS3method(generic, signature, old, envir = frame), frame)
39+
}
40+
41+
#' @rdname local_mocked_s3_method
42+
#' @export
43+
local_mocked_s4_method <- function(
44+
generic,
45+
signature,
46+
definition,
47+
frame = caller_env()
48+
) {
49+
check_string(generic)
50+
check_character(signature)
51+
check_function(definition)
52+
53+
old <- getMethod(generic, signature, optional = TRUE)
54+
if (is.null(old)) {
55+
name <- paste0(generic, "(", paste0(signature, collapse = ","), ")")
56+
cli::cli_abort(
57+
"Can't find existing S4 method {.code {name}}."
58+
)
59+
}
60+
setMethod(generic, signature, definition, where = topenv(frame))
61+
withr::defer(setMethod(generic, signature, old, where = topenv(frame)), frame)
62+
}
63+
64+
65+
#' Mock an R6 class
66+
#'
67+
#' This function allows you to temporarily override an R6 class definition.
68+
#' It works by creating a subclass then using [local_mocked_bindings()] to
69+
#' temporarily replace the original definition. This means that it will not
70+
#' affect subclasses of the original class; please file an issue if you need
71+
#' this.
72+
#'
73+
#' @export
74+
#' @param class An R6 class definition.
75+
#' @param public,private A named list of public and private methods/data.
76+
#' @inheritParams local_mocked_s3_method
77+
local_mocked_r6_class <- function(
78+
class,
79+
public = list(),
80+
private = list(),
81+
frame = caller_env()
82+
) {
83+
if (!inherits(class, "R6ClassGenerator")) {
84+
stop_input_type(class, "an R6 class definition")
85+
}
86+
if (!is.list(public)) {
87+
stop_input_type(public, "a list")
88+
}
89+
if (!is.list(private)) {
90+
stop_input_type(private, "a list")
91+
}
92+
93+
mocked_class <- mock_r6_class(class, public, private)
94+
local_mocked_bindings("{class$classname}" := mocked_class, .env = frame)
95+
}
96+
97+
mock_r6_class <- function(class, public = list(), private = list()) {
98+
R6::R6Class(
99+
paste0("Mocked", class$classname),
100+
inherit = class,
101+
private = private,
102+
public = public
103+
)
104+
}
105+
106+
# For testing ------------------------------------------------------------------
107+
108+
TestMockClass <- R6::R6Class(
109+
"TestMockClass",
110+
public = list(
111+
sum = function() {
112+
self$public_fun() +
113+
self$public_val +
114+
private$private_fun() +
115+
private$private_val
116+
},
117+
public_fun = function() 1,
118+
public_val = 20
119+
),
120+
private = list(
121+
private_fun = function() 300,
122+
private_val = 4000
123+
)
124+
)
125+
126+
TestMockPerson <- methods::setClass(
127+
"TestMockPerson",
128+
slots = c(name = "character", age = "numeric")
129+
)
130+
methods::setGeneric("mock_age", function(x) standardGeneric("mock_age"))
131+
methods::setMethod("mock_age", "TestMockPerson", function(x) x@age)

R/mock2.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
#'
2424
#' They are described in turn below.
2525
#'
26+
#' (To mock S3 & S4 methods and R6 classes see [local_mocked_s3_method()],
27+
#' [local_mocked_s4_method()], and [local_mocked_r6_class()].)
28+
#'
2629
#' ## Internal & imported functions
2730
#'
2831
#' You mock internal and imported functions the same way. For example, take

_pkgdown.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ reference:
6868

6969
- title: Mocking
7070
contents:
71-
- with_mocked_bindings
72-
- starts_with("mock_")
71+
- local_mocked_bindings
72+
- local_mocked_s3_method
73+
- local_mocked_r6_class
74+
- mock_output_sequence
7375

7476
- title: Custom expectations
7577
contents:

man/local_mocked_bindings.Rd

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/local_mocked_r6_class.Rd

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/local_mocked_s3_method.Rd

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/mock-oo.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# validates its inputs
2+
3+
Code
4+
local_mocked_s3_method(1)
5+
Condition
6+
Error in `local_mocked_s3_method()`:
7+
! `generic` must be a single string, not the number 1.
8+
Code
9+
local_mocked_s3_method("mean", 1)
10+
Condition
11+
Error in `local_mocked_s3_method()`:
12+
! `signature` must be a single string, not the number 1.
13+
Code
14+
local_mocked_s3_method("mean", "bar", 1)
15+
Condition
16+
Error in `local_mocked_s3_method()`:
17+
! `definition` must be a function, not the number 1.
18+
Code
19+
local_mocked_s3_method("mean", "bar", function() { })
20+
Condition
21+
Error in `local_mocked_s3_method()`:
22+
! Can't find existing S3 method `mean.bar()`.
23+
24+
---
25+
26+
Code
27+
local_mocked_s4_method(1)
28+
Condition
29+
Error in `local_mocked_s4_method()`:
30+
! `generic` must be a single string, not the number 1.
31+
Code
32+
local_mocked_s4_method("mean", 1)
33+
Condition
34+
Error in `local_mocked_s4_method()`:
35+
! `signature` must be a character vector, not the number 1.
36+
Code
37+
local_mocked_s4_method("mean", "bar", 1)
38+
Condition
39+
Error in `local_mocked_s4_method()`:
40+
! `definition` must be a function, not the number 1.
41+
Code
42+
local_mocked_s4_method("mean", "bar", function() { })
43+
Condition
44+
Error in `local_mocked_s4_method()`:
45+
! Can't find existing S4 method `mean(bar)`.
46+
47+
---
48+
49+
Code
50+
local_mocked_r6_class(mean)
51+
Condition
52+
Error in `local_mocked_r6_class()`:
53+
! `class` must be an R6 class definition, not a function.
54+
Code
55+
local_mocked_r6_class(TestMockClass, public = 1)
56+
Condition
57+
Error in `local_mocked_r6_class()`:
58+
! `public` must be a list, not the number 1.
59+
Code
60+
local_mocked_r6_class(TestMockClass, private = 1)
61+
Condition
62+
Error in `local_mocked_r6_class()`:
63+
! `private` must be a list, not the number 1.
64+

tests/testthat/test-mock-oo.R

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# S3 --------------------------------------------------------------------------
2+
3+
test_that("can mock S3 methods", {
4+
x <- as.POSIXlt(Sys.time())
5+
6+
local({
7+
local_mocked_s3_method("length", "POSIXlt", function(x) 42)
8+
expect_length(x, 42)
9+
})
10+
11+
expect_length(x, 1)
12+
})
13+
14+
test_that("validates its inputs", {
15+
expect_snapshot(error = TRUE, {
16+
local_mocked_s3_method(1)
17+
local_mocked_s3_method("mean", 1)
18+
local_mocked_s3_method("mean", "bar", 1)
19+
local_mocked_s3_method("mean", "bar", function() {})
20+
})
21+
})
22+
23+
# S4 --------------------------------------------------------------------------
24+
25+
test_that("can mock S4 methods", {
26+
jim <- TestMockPerson(name = "Jim", age = 32)
27+
28+
local({
29+
local_mocked_s4_method("mock_age", "TestMockPerson", function(x) 42)
30+
expect_equal(mock_age(jim), 42)
31+
})
32+
33+
expect_equal(mock_age(jim), 32)
34+
})
35+
36+
37+
test_that("validates its inputs", {
38+
expect_snapshot(error = TRUE, {
39+
local_mocked_s4_method(1)
40+
local_mocked_s4_method("mean", 1)
41+
local_mocked_s4_method("mean", "bar", 1)
42+
local_mocked_s4_method("mean", "bar", function() {})
43+
})
44+
})
45+
46+
# R6 --------------------------------------------------------------------------
47+
48+
test_that("can mock R6 methods", {
49+
local({
50+
local_mocked_r6_class(TestMockClass, public = list(sum = function() 2))
51+
obj <- TestMockClass$new()
52+
expect_equal(obj$sum(), 2)
53+
})
54+
55+
obj <- TestMockClass$new()
56+
expect_equal(obj$sum(), 4321)
57+
})
58+
59+
test_that("can mock all R6 components", {
60+
local_mocked_r6_class(
61+
TestMockClass,
62+
public = list(public_fun = function() 0, public_val = 0),
63+
private = list(private_fun = function() 0, private_val = 0)
64+
)
65+
obj <- TestMockClass$new()
66+
expect_equal(obj$sum(), 0)
67+
})
68+
69+
test_that("validates its inputs", {
70+
expect_snapshot(error = TRUE, {
71+
local_mocked_r6_class(mean)
72+
local_mocked_r6_class(TestMockClass, public = 1)
73+
local_mocked_r6_class(TestMockClass, private = 1)
74+
})
75+
})

0 commit comments

Comments
 (0)