diff --git a/DESCRIPTION b/DESCRIPTION index 0d38b02d9..5939b1c6e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -55,4 +55,4 @@ Config/testthat/parallel: true Config/testthat/start-first: watcher, parallel* Encoding: UTF-8 Roxygen: list(markdown = TRUE, r6 = FALSE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 diff --git a/NAMESPACE b/NAMESPACE index e13d30231..0cac60fb9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -84,6 +84,7 @@ export(expect_equal) export(expect_equal_to_reference) export(expect_equivalent) export(expect_error) +export(expect_error_forwarded) export(expect_failure) export(expect_false) export(expect_gt) @@ -102,6 +103,7 @@ export(expect_lte) export(expect_mapequal) export(expect_match) export(expect_message) +export(expect_message_forwarded) export(expect_more_than) export(expect_named) export(expect_no_condition) @@ -130,6 +132,7 @@ export(expect_type) export(expect_vector) export(expect_visible) export(expect_warning) +export(expect_warning_forwarded) export(expectation) export(fail) export(find_test_scripts) diff --git a/NEWS.md b/NEWS.md index 6eb90a984..687d45e43 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # testthat (development version) +* Add new error, warning, and message capturing expectations for conditions from + other packages. `expect_error_forwarded()`, `expect_warning_forwarded()`, and + `expect_message_forwarded()` capture errors, warnings, and messages that come + from other code to match the message that is not in the control of the package + being tested (@billdenney, #1927). + # testthat 3.2.1 * Fix incorrect format string detected by latest R-devel. Fix thanks to diff --git a/R/expect-condition-forwarded.R b/R/expect-condition-forwarded.R new file mode 100644 index 000000000..e2ce9ccf9 --- /dev/null +++ b/R/expect-condition-forwarded.R @@ -0,0 +1,90 @@ +#' Does code throw an error, warning, or message that is the same as another? +#' +#' @description +#' `expect_error_forwarded()`, `expect_warning_forwarded()`, and +#' `expect_message_forwarded()`, check that code throws an error, warning, +#' message, or condition with a message that matches an `expected` value from an +#' example. These functions are useful to test that a condition matches one +#' from another package that you do not control. +#' +#' @export +#' @family expectations +#' @inheritParams expect_that +#' @param expected A call that causes the expected error, warning, or message. +#' It is usually a call to another package than the one being tested. +#' @return The value of the first argument +#' @examples +#' myfun <- function(x) { +#' stopifnot(length(x) == 1) +#' } +#' +#' expect_error_forwarded( +#' myfun(x = 1:2), +#' { +#' x <- 1:2 +#' stopifnot(length(x) == 1) +#' } +#' ) +expect_error_forwarded <- function(object, expected, label = NULL) { + message <- error_message(expected) + expect_error({{ object }}, regexp = message, fixed = TRUE, label = label) +} + +#' @export +#' @rdname expect_error_forwarded +expect_warning_forwarded <- function(object, expected, label = NULL) { + message <- warning_message(expected) + expect_warning({{ object }}, regexp = message, fixed = TRUE, label = label) +} + +#' @export +#' @rdname expect_error_forwarded +expect_message_forwarded <- function(object, expected, label = NULL) { + message <- message_message(expected) + expect_message({{ object }}, regexp = message, fixed = TRUE, label = label) +} + +error_message <- function(code) { + out <- tryCatch( + { + code + NULL + }, + error = function(e) conditionMessage(e) + ) + if (is.null(out)) { + stop("No error thrown") + } else { + out + } +} + +warning_message <- function(code) { + out <- tryCatch( + { + code + NULL + }, + warning = function(e) conditionMessage(e) + ) + if (is.null(out)) { + stop("No warning thrown") + } else { + out + } +} + +message_message <- function(code) { + out <- tryCatch( + { + code + NULL + }, + message = function(e) conditionMessage(e) + ) + if (is.null(out)) { + stop("No message thrown") + } else { + out + } +} diff --git a/R/expect-condition.R b/R/expect-condition.R index 7bf2395d4..e1cd25d88 100644 --- a/R/expect-condition.R +++ b/R/expect-condition.R @@ -6,6 +6,10 @@ #' or condition with a message that matches `regexp`, or a class that inherits #' from `class`. See below for more details. #' +#' If you need to test that an error, warning, or message is the same as one +#' given by another package, then see the [expect_error_forwarded()], +#' [expect_warning_forwarded()], and [expect_message_forwarded()] functions. +#' #' In the 3rd edition, these functions match (at most) a single condition. All #' additional and non-matching (if `regexp` or `class` are used) conditions #' will bubble up outside the expectation. If these additional conditions diff --git a/_pkgdown.yml b/_pkgdown.yml index 209eea3d6..77c7fcf81 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -38,6 +38,7 @@ reference: - expect_invisible - expect_output - expect_silent + - expect_error_forwarded - local_reproducible_output - title: Snapshot testing diff --git a/man/comparison-expectations.Rd b/man/comparison-expectations.Rd index 5cbee6931..ca415bdf8 100644 --- a/man/comparison-expectations.Rd +++ b/man/comparison-expectations.Rd @@ -43,6 +43,7 @@ expect_gt(9, 10) Other expectations: \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/equality-expectations.Rd b/man/equality-expectations.Rd index e22afd695..aa8a9672a 100644 --- a/man/equality-expectations.Rd +++ b/man/equality-expectations.Rd @@ -86,6 +86,7 @@ expect_equal(sqrt(2) ^ 2, 2) Other expectations: \code{\link{comparison-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_error.Rd b/man/expect_error.Rd index 50851564b..8156c0d40 100644 --- a/man/expect_error.Rd +++ b/man/expect_error.Rd @@ -105,6 +105,10 @@ the captured condition. or condition with a message that matches \code{regexp}, or a class that inherits from \code{class}. See below for more details. +If you need to test that an error, warning, or message is the same as one +given by another package, then see the \code{\link[=expect_error_forwarded]{expect_error_forwarded()}}, +\code{\link[=expect_warning_forwarded]{expect_warning_forwarded()}}, and \code{\link[=expect_message_forwarded]{expect_message_forwarded()}} functions. + In the 3rd edition, these functions match (at most) a single condition. All additional and non-matching (if \code{regexp} or \code{class} are used) conditions will bubble up outside the expectation. If these additional conditions @@ -186,6 +190,7 @@ that code runs without errors/warnings/messages/conditions. Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_error_forwarded.Rd b/man/expect_error_forwarded.Rd new file mode 100644 index 000000000..93010f199 --- /dev/null +++ b/man/expect_error_forwarded.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/expect-condition-forwarded.R +\name{expect_error_forwarded} +\alias{expect_error_forwarded} +\alias{expect_warning_forwarded} +\alias{expect_message_forwarded} +\title{Does code throw an error, warning, or message that is the same as another?} +\usage{ +expect_error_forwarded(object, expected, label = NULL) + +expect_warning_forwarded(object, expected, label = NULL) + +expect_message_forwarded(object, expected, label = NULL) +} +\arguments{ +\item{object}{Object to test. + +Supports limited unquoting to make it easier to generate readable failures +within a function or for loop. See \link{quasi_label} for more details.} + +\item{expected}{A call that causes the expected error, warning, or message. +It is usually a call to another package than the one being tested.} + +\item{label}{Used to customise failure messages. For expert use only.} +} +\value{ +The value of the first argument +} +\description{ +\code{expect_error_forwarded()}, \code{expect_warning_forwarded()}, and +\code{expect_message_forwarded()}, check that code throws an error, warning, +message, or condition with a message that matches an \code{expected} value from an +example. These functions are useful to test that a condition matches one +from another package that you do not control. +} +\examples{ +myfun <- function(x) { + stopifnot(length(x) == 1) +} + +expect_error_forwarded( + myfun(x = 1:2), + { + x <- 1:2 + stopifnot(length(x) == 1) + } +) +} +\seealso{ +Other expectations: +\code{\link{comparison-expectations}}, +\code{\link{equality-expectations}}, +\code{\link{expect_error}()}, +\code{\link{expect_length}()}, +\code{\link{expect_match}()}, +\code{\link{expect_named}()}, +\code{\link{expect_null}()}, +\code{\link{expect_output}()}, +\code{\link{expect_reference}()}, +\code{\link{expect_silent}()}, +\code{\link{inheritance-expectations}}, +\code{\link{logical-expectations}} +} +\concept{expectations} diff --git a/man/expect_length.Rd b/man/expect_length.Rd index 4e65e8069..2d1004ff1 100644 --- a/man/expect_length.Rd +++ b/man/expect_length.Rd @@ -32,6 +32,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, \code{\link{expect_null}()}, diff --git a/man/expect_match.Rd b/man/expect_match.Rd index 1272f53e3..151a71147 100644 --- a/man/expect_match.Rd +++ b/man/expect_match.Rd @@ -91,6 +91,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_named}()}, \code{\link{expect_null}()}, diff --git a/man/expect_named.Rd b/man/expect_named.Rd index bfa9ad9fc..a1e36e317 100644 --- a/man/expect_named.Rd +++ b/man/expect_named.Rd @@ -56,6 +56,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_null}()}, diff --git a/man/expect_null.Rd b/man/expect_null.Rd index 45b1adb12..9ad98e2d5 100644 --- a/man/expect_null.Rd +++ b/man/expect_null.Rd @@ -34,6 +34,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_output.Rd b/man/expect_output.Rd index c7d789f6e..259e9e50e 100644 --- a/man/expect_output.Rd +++ b/man/expect_output.Rd @@ -68,6 +68,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_reference.Rd b/man/expect_reference.Rd index e5a4b120e..da90fc844 100644 --- a/man/expect_reference.Rd +++ b/man/expect_reference.Rd @@ -44,6 +44,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_silent.Rd b/man/expect_silent.Rd index 69688a2ec..460349aea 100644 --- a/man/expect_silent.Rd +++ b/man/expect_silent.Rd @@ -35,6 +35,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/inheritance-expectations.Rd b/man/inheritance-expectations.Rd index 1b2191620..6452dd8ff 100644 --- a/man/inheritance-expectations.Rd +++ b/man/inheritance-expectations.Rd @@ -68,6 +68,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/logical-expectations.Rd b/man/logical-expectations.Rd index da38c3bde..826255114 100644 --- a/man/logical-expectations.Rd +++ b/man/logical-expectations.Rd @@ -52,6 +52,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/tests/testthat/test-expect-condition-forwarded.R b/tests/testthat/test-expect-condition-forwarded.R new file mode 100644 index 000000000..9b0d3a050 --- /dev/null +++ b/tests/testthat/test-expect-condition-forwarded.R @@ -0,0 +1,81 @@ +test_that("expect_error_forwarded general use", { + expect_success( + expect_error_forwarded( + stop(), + stop() + ) + ) + + # Matches + expect_success( + expect_error_forwarded( + stop("foo"), + stop("foo") + ) + ) + # Does not match + expect_error( + expect_error_forwarded( + stop("foo"), + stop("bar") + ) + ) + + # Matches + expect_success( + expect_warning_forwarded( + warning("foo"), + warning("foo") + ) + ) + # Does not match + expect_warning( + expect_error( + expect_warning_forwarded( + warning("foo"), + warning("bar") + ), + regexp = '`warning("foo")` did not throw the expected warning.', + fixed = TRUE + ), + regexp = "foo" + ) + + # Matches + expect_success( + expect_message_forwarded( + message("foo"), + message("foo") + ) + ) + # Does not match + expect_message( + expect_error( + expect_message_forwarded( + message("foo"), + message("bar") + ), + regexp = '`message("foo")` did not throw the expected message.', + fixed = TRUE + ), + regexp = "foo" + ) +}) + +test_that("expect_*_forwarded with no condition generated ", { + expect_error( + expect_error_forwarded(1, 1), + regexp = "No error thrown", + fixed = TRUE + ) + expect_error( + expect_warning_forwarded(1, 1), + regexp = "No warning thrown", + fixed = TRUE + ) + expect_error( + expect_message_forwarded(1, 1), + regexp = "No message thrown", + fixed = TRUE + ) +})