Skip to content

Commit

Permalink
Implement mock_output_sequence() (#2061)
Browse files Browse the repository at this point in the history
Similar to {mockery}'s multiple return values
  • Loading branch information
maelle authored Feb 28, 2025
1 parent ae35cc9 commit 30f5b11
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 2 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ Config/testthat/parallel: true
Config/testthat/start-first: watcher, parallel*
Encoding: UTF-8
Roxygen: list(markdown = TRUE, r6 = FALSE)
RoxygenNote: 7.3.2
RoxygenNote: 7.3.2.9000
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export(local_test_context)
export(local_test_directory)
export(make_expectation)
export(matches)
export(mock_output_sequence)
export(new_expectation)
export(not)
export(prints_text)
Expand Down
49 changes: 49 additions & 0 deletions R/mock2-helpers.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#' Mock a sequence of output from a function
#'
#' Specify multiple return values for mocking
#'
#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Values to return in sequence.
#' @param recycle whether to recycle. If `TRUE`, once all values have been returned,
#' they will be returned again in sequence.
#'
#' @return A function that you can use within `local_mocked_bindings()` and
#' `with_mocked_bindings()`
#' @export
#'
#' @examples
#' # inside local_mocked_bindings()
#' \dontrun{
#' local_mocked_bindings(readline = mock_output_sequence("3", "This is a note", "n"))
#' }
#' # for understanding
#' mocked_sequence <- mock_output_sequence("3", "This is a note", "n")
#' mocked_sequence()
#' mocked_sequence()
#' mocked_sequence()
#' try(mocked_sequence())
#' recycled_mocked_sequence <- mock_output_sequence(
#' "3", "This is a note", "n",
#' recycle = TRUE
#' )
#' recycled_mocked_sequence()
#' recycled_mocked_sequence()
#' recycled_mocked_sequence()
#' recycled_mocked_sequence()
#' @family mocking
mock_output_sequence <- function(..., recycle = FALSE) {
values <- rlang::list2(...)
i <- 1
function(...) {
if (i > length(values) && !recycle) {
cli::cli_abort(c(
"Can't find value for {i}th iteration.",
i = "{.arg ...} has only {length(values)} values.",
i = "You can set {.arg recycle} to {.code TRUE}."
))
}
index <- (i - 1) %% length(values) + 1
value <- rep_len(values, length.out = index)[[index]]
i <<- i + 1
value
}
}
12 changes: 12 additions & 0 deletions R/mock2.R
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@
#' my_wrapper = function(...) "new_value"
#' )
#' ```
#'
#' ## Multiple return values / sequence of outputs
#'
#' To mock a function that returns different values in sequence,
#' for instance an API call whose status would be 502 then 200,
#' or an user intput to `readline()`, you can use [mock_output_sequence()]
#'
#' ```R
#' local_mocked_bindings(readline = mock_output_sequence("3", "This is a note", "n"))
#' ```
#'
#' @export
#' @param ... Name-value pairs providing new values (typically functions) to
#' temporarily replace the named bindings.
Expand All @@ -103,6 +114,7 @@
#' under active development (i.e. loaded with [pkgload::load_all()]).
#' We don't recommend using this to mock functions in other packages,
#' as you should not modify namespaces that you don't own.
#' @family mocking
local_mocked_bindings <- function(..., .package = NULL, .env = caller_env()) {
bindings <- list2(...)
check_bindings(bindings)
Expand Down
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ reference:
- title: Mocking
contents:
- with_mocked_bindings
- starts_with("mock_")

- title: Expectation internals
contents:
Expand Down
2 changes: 1 addition & 1 deletion man/expect_vector.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions man/local_mocked_bindings.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions man/mock_output_sequence.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions tests/testthat/_snaps/mock2-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# mock_output_sequence() works

Code
mocked_sequence()
Condition
Error in `mocked_sequence()`:
! Can't find value for 4th iteration.
i `...` has only 3 values.
i You can set `recycle` to `TRUE`.

29 changes: 29 additions & 0 deletions tests/testthat/test-mock2-helpers.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
test_that("mock_output_sequence() works", {
mocked_sequence <- mock_output_sequence("3", "This is a note", "n")
expect_equal(mocked_sequence(), "3")
expect_equal(mocked_sequence(), "This is a note")
expect_equal(mocked_sequence(), "n")
expect_snapshot(mocked_sequence(), error = TRUE)
})

test_that("mock_output_sequence() works -- list", {
x <- list("3", "This is a note", "n")
mocked_sequence <- mock_output_sequence(!!!x)
expect_equal(mocked_sequence(), "3")
expect_equal(mocked_sequence(), "This is a note")
expect_equal(mocked_sequence(), "n")
})

test_that("mock_output_sequence()'s recycling works", {
mocked_sequence <- mock_output_sequence(
"3", "This is a note", "n",
recycle = TRUE
)
expect_equal(mocked_sequence(), "3")
expect_equal(mocked_sequence(), "This is a note")
expect_equal(mocked_sequence(), "n")
expect_equal(mocked_sequence(), "3")
expect_equal(mocked_sequence(), "This is a note")
expect_equal(mocked_sequence(), "n")
})

0 comments on commit 30f5b11

Please sign in to comment.