diff --git a/DESCRIPTION b/DESCRIPTION index 150a46d8b..6c73d86c2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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 diff --git a/NAMESPACE b/NAMESPACE index 8f0286c3b..634b60815 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) diff --git a/R/mock2-helpers.R b/R/mock2-helpers.R new file mode 100644 index 000000000..cd80f5d57 --- /dev/null +++ b/R/mock2-helpers.R @@ -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 + } +} diff --git a/R/mock2.R b/R/mock2.R index a351cb88e..29cfcda43 100644 --- a/R/mock2.R +++ b/R/mock2.R @@ -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. @@ -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) diff --git a/_pkgdown.yml b/_pkgdown.yml index 581dd8fbe..418d8c7e9 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -72,6 +72,7 @@ reference: - title: Mocking contents: - with_mocked_bindings + - starts_with("mock_") - title: Expectation internals contents: diff --git a/man/expect_vector.Rd b/man/expect_vector.Rd index cf44af85f..a33ef14b6 100644 --- a/man/expect_vector.Rd +++ b/man/expect_vector.Rd @@ -24,7 +24,7 @@ means that it used the vctrs of \code{ptype} (prototype) and \code{size}. See details in \url{https://vctrs.r-lib.org/articles/type-size.html} } \examples{ -\dontshow{if (requireNamespace("vctrs")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +\dontshow{if (requireNamespace("vctrs")) withAutoprint(\{ # examplesIf} expect_vector(1:10, ptype = integer(), size = 10) show_failure(expect_vector(1:10, ptype = integer(), size = 5)) show_failure(expect_vector(1:10, ptype = character(), size = 5)) diff --git a/man/local_mocked_bindings.Rd b/man/local_mocked_bindings.Rd index d8df2b34f..ba0d0a004 100644 --- a/man/local_mocked_bindings.Rd +++ b/man/local_mocked_bindings.Rd @@ -113,5 +113,20 @@ local_mocked_bindings( ) }\if{html}{\out{}} } + +\subsection{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 \code{readline()}, you can use \code{\link[=mock_output_sequence]{mock_output_sequence()}} + +\if{html}{\out{
}}\preformatted{local_mocked_bindings(readline = mock_output_sequence("3", "This is a note", "n")) +}\if{html}{\out{
}} +} } +\seealso{ +Other mocking: +\code{\link{mock_output_sequence}()} +} +\concept{mocking} diff --git a/man/mock_output_sequence.Rd b/man/mock_output_sequence.Rd new file mode 100644 index 000000000..9ca2f1395 --- /dev/null +++ b/man/mock_output_sequence.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/mock2-helpers.R +\name{mock_output_sequence} +\alias{mock_output_sequence} +\title{Mock a sequence of output from a function} +\usage{ +mock_output_sequence(..., recycle = FALSE) +} +\arguments{ +\item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> Values to return in sequence.} + +\item{recycle}{whether to recycle. If \code{TRUE}, once all values have been returned, +they will be returned again in sequence.} +} +\value{ +A function that you can use within \code{local_mocked_bindings()} and +\code{with_mocked_bindings()} +} +\description{ +Specify multiple return values for mocking +} +\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() +} +\seealso{ +Other mocking: +\code{\link{local_mocked_bindings}()} +} +\concept{mocking} diff --git a/tests/testthat/_snaps/mock2-helpers.md b/tests/testthat/_snaps/mock2-helpers.md new file mode 100644 index 000000000..21a88b2b8 --- /dev/null +++ b/tests/testthat/_snaps/mock2-helpers.md @@ -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`. + diff --git a/tests/testthat/test-mock2-helpers.R b/tests/testthat/test-mock2-helpers.R new file mode 100644 index 000000000..342cf9de2 --- /dev/null +++ b/tests/testthat/test-mock2-helpers.R @@ -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") +}) +