Skip to content

Commit 30f5b11

Browse files
authored
Implement mock_output_sequence() (#2061)
Similar to {mockery}'s multiple return values
1 parent ae35cc9 commit 30f5b11

10 files changed

+165
-2
lines changed

DESCRIPTION

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ Config/testthat/parallel: true
5656
Config/testthat/start-first: watcher, parallel*
5757
Encoding: UTF-8
5858
Roxygen: list(markdown = TRUE, r6 = FALSE)
59-
RoxygenNote: 7.3.2
59+
RoxygenNote: 7.3.2.9000

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export(local_test_context)
164164
export(local_test_directory)
165165
export(make_expectation)
166166
export(matches)
167+
export(mock_output_sequence)
167168
export(new_expectation)
168169
export(not)
169170
export(prints_text)

R/mock2-helpers.R

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#' Mock a sequence of output from a function
2+
#'
3+
#' Specify multiple return values for mocking
4+
#'
5+
#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Values to return in sequence.
6+
#' @param recycle whether to recycle. If `TRUE`, once all values have been returned,
7+
#' they will be returned again in sequence.
8+
#'
9+
#' @return A function that you can use within `local_mocked_bindings()` and
10+
#' `with_mocked_bindings()`
11+
#' @export
12+
#'
13+
#' @examples
14+
#' # inside local_mocked_bindings()
15+
#' \dontrun{
16+
#' local_mocked_bindings(readline = mock_output_sequence("3", "This is a note", "n"))
17+
#' }
18+
#' # for understanding
19+
#' mocked_sequence <- mock_output_sequence("3", "This is a note", "n")
20+
#' mocked_sequence()
21+
#' mocked_sequence()
22+
#' mocked_sequence()
23+
#' try(mocked_sequence())
24+
#' recycled_mocked_sequence <- mock_output_sequence(
25+
#' "3", "This is a note", "n",
26+
#' recycle = TRUE
27+
#' )
28+
#' recycled_mocked_sequence()
29+
#' recycled_mocked_sequence()
30+
#' recycled_mocked_sequence()
31+
#' recycled_mocked_sequence()
32+
#' @family mocking
33+
mock_output_sequence <- function(..., recycle = FALSE) {
34+
values <- rlang::list2(...)
35+
i <- 1
36+
function(...) {
37+
if (i > length(values) && !recycle) {
38+
cli::cli_abort(c(
39+
"Can't find value for {i}th iteration.",
40+
i = "{.arg ...} has only {length(values)} values.",
41+
i = "You can set {.arg recycle} to {.code TRUE}."
42+
))
43+
}
44+
index <- (i - 1) %% length(values) + 1
45+
value <- rep_len(values, length.out = index)[[index]]
46+
i <<- i + 1
47+
value
48+
}
49+
}

R/mock2.R

+12
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@
9292
#' my_wrapper = function(...) "new_value"
9393
#' )
9494
#' ```
95+
#'
96+
#' ## Multiple return values / sequence of outputs
97+
#'
98+
#' To mock a function that returns different values in sequence,
99+
#' for instance an API call whose status would be 502 then 200,
100+
#' or an user intput to `readline()`, you can use [mock_output_sequence()]
101+
#'
102+
#' ```R
103+
#' local_mocked_bindings(readline = mock_output_sequence("3", "This is a note", "n"))
104+
#' ```
105+
#'
95106
#' @export
96107
#' @param ... Name-value pairs providing new values (typically functions) to
97108
#' temporarily replace the named bindings.
@@ -103,6 +114,7 @@
103114
#' under active development (i.e. loaded with [pkgload::load_all()]).
104115
#' We don't recommend using this to mock functions in other packages,
105116
#' as you should not modify namespaces that you don't own.
117+
#' @family mocking
106118
local_mocked_bindings <- function(..., .package = NULL, .env = caller_env()) {
107119
bindings <- list2(...)
108120
check_bindings(bindings)

_pkgdown.yml

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ reference:
7272
- title: Mocking
7373
contents:
7474
- with_mocked_bindings
75+
- starts_with("mock_")
7576

7677
- title: Expectation internals
7778
contents:

man/expect_vector.Rd

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/local_mocked_bindings.Rd

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/mock_output_sequence.Rd

+46
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# mock_output_sequence() works
2+
3+
Code
4+
mocked_sequence()
5+
Condition
6+
Error in `mocked_sequence()`:
7+
! Can't find value for 4th iteration.
8+
i `...` has only 3 values.
9+
i You can set `recycle` to `TRUE`.
10+

tests/testthat/test-mock2-helpers.R

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
test_that("mock_output_sequence() works", {
2+
mocked_sequence <- mock_output_sequence("3", "This is a note", "n")
3+
expect_equal(mocked_sequence(), "3")
4+
expect_equal(mocked_sequence(), "This is a note")
5+
expect_equal(mocked_sequence(), "n")
6+
expect_snapshot(mocked_sequence(), error = TRUE)
7+
})
8+
9+
test_that("mock_output_sequence() works -- list", {
10+
x <- list("3", "This is a note", "n")
11+
mocked_sequence <- mock_output_sequence(!!!x)
12+
expect_equal(mocked_sequence(), "3")
13+
expect_equal(mocked_sequence(), "This is a note")
14+
expect_equal(mocked_sequence(), "n")
15+
})
16+
17+
test_that("mock_output_sequence()'s recycling works", {
18+
mocked_sequence <- mock_output_sequence(
19+
"3", "This is a note", "n",
20+
recycle = TRUE
21+
)
22+
expect_equal(mocked_sequence(), "3")
23+
expect_equal(mocked_sequence(), "This is a note")
24+
expect_equal(mocked_sequence(), "n")
25+
expect_equal(mocked_sequence(), "3")
26+
expect_equal(mocked_sequence(), "This is a note")
27+
expect_equal(mocked_sequence(), "n")
28+
})
29+

0 commit comments

Comments
 (0)