diff --git a/NAMESPACE b/NAMESPACE index 426dbf1b4..63d1572e5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -79,6 +79,9 @@ export(equals_reference) export(evaluate_promise) export(exp_signal) export(expect) +export(expect_all_equal) +export(expect_all_false) +export(expect_all_true) export(expect_condition) export(expect_contains) export(expect_cpp_tests_pass) diff --git a/NEWS.md b/NEWS.md index 207716bf1..3137332a6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # testthat (development version) +* `expect_all_equal()`, `expect_all_true()`, and `expect_all_false()` are a new family of expectations that checks that every element of a vector has the same value. Compared to using `expect_true(all(...))` they give better failure messages (#1836, #2235). * Expectations now consistently return the value of the first argument, regardless of whether the expectation succeeds or fails. The primary exception are `expect_message()` and friends which will return the condition. This shouldn't affect existing tests, but will make failures clearer when you chain together multiple expectations (#2246). * `set_state_inspector()` gains `tolerance` argument and ignores minor FP differences by default (@mcol, #2237). * `expect_vector()` fails, instead of erroring, if `object` is not a vector (@plietar, #2224). diff --git a/R/expect-all.R b/R/expect-all.R new file mode 100644 index 000000000..cce8ec32c --- /dev/null +++ b/R/expect-all.R @@ -0,0 +1,69 @@ +#' Do you expect every value in a vector to have this value? +#' +#' These expectations are similar to `expect_true(all(x == "x"))`, +#' `expect_true(all(x))` and `expect_true(all(!x))` but give more informative +#' failure messages if the expectations are not met. +#' +#' @inheritParams expect_equal +#' @export +#' @examples +#' x1 <- c(1, 1, 1, 1, 1, 1) +#' expect_all_equal(x1, 1) +#' +#' x2 <- c(1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2) +#' show_failure(expect_all_equal(x2, 1)) +#' +#' # expect_all_true() and expect_all_false() are helpers for common cases +#' set.seed(1016) +#' show_failure(expect_all_true(rpois(100, 10) < 20)) +#' show_failure(expect_all_false(rpois(100, 10) > 20)) +expect_all_equal <- function(object, expected) { + act <- quasi_label(enquo(object)) + exp <- quasi_label(enquo(expected)) + + expect_all_equal_(act, exp) + invisible(act$val) +} + +#' @export +#' @rdname expect_all_equal +expect_all_true <- function(object) { + act <- quasi_label(enquo(object)) + exp <- labelled_value(TRUE, "TRUE") + + expect_all_equal_(act, exp) + invisible(act$val) +} + +#' @export +#' @rdname expect_all_equal +expect_all_false <- function(object) { + act <- quasi_label(enquo(object)) + exp <- labelled_value(FALSE, "FALSE") + + expect_all_equal_(act, exp) + invisible(act$val) +} + + +expect_all_equal_ <- function(act, exp, trace_env = caller_env()) { + check_vector(act$val, error_call = trace_env, error_arg = "object") + if (length(act$val) == 0) { + cli::cli_abort("{.arg object} must not be empty.", call = trace_env) + } + + check_vector(exp$val, error_call = trace_env, error_arg = "expected") + if (length(exp$val) != 1) { + cli::cli_abort("{.arg expected} must be length 1.", call = trace_env) + } + + exp$val <- rep(exp$val, length(act$val)) + names(exp$val) <- names(act$val) + expect_waldo_equal_( + "Expected every element of %s to equal %s.", + act, + exp, + tolerance = testthat_tolerance(), + trace_env = trace_env + ) +} diff --git a/R/expect-constant.R b/R/expect-constant.R index 2640f2c88..22267caae 100644 --- a/R/expect-constant.R +++ b/R/expect-constant.R @@ -12,17 +12,17 @@ #' @examples #' expect_true(2 == 2) #' # Failed expectations will throw an error -#' \dontrun{ -#' expect_true(2 != 2) -#' } -#' expect_true(!(2 != 2)) -#' # or better: -#' expect_false(2 != 2) +#' show_failure(expect_true(2 != 2)) #' -#' a <- 1:3 -#' expect_true(length(a) == 3) -#' # but better to use more specific expectation, if available -#' expect_equal(length(a), 3) +#' # where possible, use more specific expectations, to get more informative +#' # error messages +#' a <- 1:4 +#' show_failure(expect_true(length(a) == 3)) +#' show_failure(expect_equal(length(a), 3)) +#' +#' x <- c(TRUE, TRUE, FALSE, TRUE) +#' show_failure(expect_true(all(x))) +#' show_failure(expect_all_true(x)) #' @name logical-expectations NULL diff --git a/_pkgdown.yml b/_pkgdown.yml index bb17b1520..0b9aa2f12 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -27,6 +27,7 @@ reference: - title: Expectations - subtitle: Values contents: + - expect_all_equal - expect_gt - expect_length - expect_match diff --git a/man/expect_all_equal.Rd b/man/expect_all_equal.Rd new file mode 100644 index 000000000..b230b3b3f --- /dev/null +++ b/man/expect_all_equal.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/expect-all.R +\name{expect_all_equal} +\alias{expect_all_equal} +\alias{expect_all_true} +\alias{expect_all_false} +\title{Do you expect every value in a vector to have this value?} +\usage{ +expect_all_equal(object, expected) + +expect_all_true(object) + +expect_all_false(object) +} +\arguments{ +\item{object, expected}{Computation and value to compare it to. + +Both arguments supports limited unquoting to make it easier to generate +readable failures within a function or for loop. See \link{quasi_label} for +more details.} +} +\description{ +These expectations are similar to \code{expect_true(all(x == "x"))}, +\code{expect_true(all(x))} and \code{expect_true(all(!x))} but give more informative +failure messages if the expectations are not met. +} +\examples{ +x1 <- c(1, 1, 1, 1, 1, 1) +expect_all_equal(x1, 1) + +x2 <- c(1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2) +show_failure(expect_all_equal(x2, 1)) + +# expect_all_true() and expect_all_false() are helpers for common cases +set.seed(1016) +show_failure(expect_all_true(rpois(100, 10) < 20)) +show_failure(expect_all_false(rpois(100, 10) > 20)) +} diff --git a/man/logical-expectations.Rd b/man/logical-expectations.Rd index ff6c90539..56f4e961c 100644 --- a/man/logical-expectations.Rd +++ b/man/logical-expectations.Rd @@ -32,17 +32,17 @@ Attributes are ignored. \examples{ expect_true(2 == 2) # Failed expectations will throw an error -\dontrun{ -expect_true(2 != 2) -} -expect_true(!(2 != 2)) -# or better: -expect_false(2 != 2) +show_failure(expect_true(2 != 2)) + +# where possible, use more specific expectations, to get more informative +# error messages +a <- 1:4 +show_failure(expect_true(length(a) == 3)) +show_failure(expect_equal(length(a), 3)) -a <- 1:3 -expect_true(length(a) == 3) -# but better to use more specific expectation, if available -expect_equal(length(a), 3) +x <- c(TRUE, TRUE, FALSE, TRUE) +show_failure(expect_true(all(x))) +show_failure(expect_all_true(x)) } \seealso{ Other expectations: diff --git a/tests/testthat/_snaps/expect-all.md b/tests/testthat/_snaps/expect-all.md new file mode 100644 index 000000000..23d2f8661 --- /dev/null +++ b/tests/testthat/_snaps/expect-all.md @@ -0,0 +1,56 @@ +# validates its inputs + + Code + expect_all_equal(mean, 1) + Condition + Error in `expect_all_equal()`: + ! `object` must be a vector, not a function. + Code + expect_all_equal(logical(), 1) + Condition + Error in `expect_all_equal()`: + ! `object` must not be empty. + Code + expect_all_equal(1:10, mean) + Condition + Error in `expect_all_equal()`: + ! `expected` must be a vector, not a function. + Code + expect_all_equal(1:10, 1:2) + Condition + Error in `expect_all_equal()`: + ! `expected` must be length 1. + +# can compare atomic vectors + + Code + expect_all_equal(x, TRUE) + Condition + Error: + ! Expected every element of `x` to equal TRUE. + Differences: + `actual[2:8]`: TRUE TRUE TRUE FALSE TRUE TRUE TRUE + `expected[2:8]`: TRUE TRUE TRUE TRUE TRUE TRUE TRUE + +# can compare named lists + + Code + expect_all_equal(x, list(1)) + Condition + Error: + ! Expected every element of `x` to equal `list(1)`. + Differences: + `actual$c`: 2.0 + `expected$c`: 1.0 + +# truncates very long differences + + Code + expect_all_equal(x, FALSE) + Condition + Error: + ! Expected every element of `x` to equal FALSE. + Differences: + `actual`: TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE + `expected`: FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE + diff --git a/tests/testthat/test-expect-all.R b/tests/testthat/test-expect-all.R new file mode 100644 index 000000000..f2a525f50 --- /dev/null +++ b/tests/testthat/test-expect-all.R @@ -0,0 +1,37 @@ +test_that("validates its inputs", { + expect_snapshot(error = TRUE, { + expect_all_equal(mean, 1) + expect_all_equal(logical(), 1) + expect_all_equal(1:10, mean) + expect_all_equal(1:10, 1:2) + }) +}) + +test_that("can compare atomic vectors", { + x <- rep(TRUE, 10) + expect_success(expect_all_equal(x, TRUE)) + + x[5] <- FALSE + expect_snapshot_failure(expect_all_equal(x, TRUE)) +}) + +test_that("can compare named lists", { + x <- list(a = 1, b = 1, c = 2, d = 1, e = 1) + expect_snapshot_failure(expect_all_equal(x, list(1))) +}) + +test_that("has tolerance enabled", { + expect_success(expect_all_equal(1, 1L)) +}) + +test_that("truncates very long differences", { + x <- rep(TRUE, 10) + expect_snapshot_failure(expect_all_equal(x, FALSE)) +}) + +test_that("has TRUE and FALSE helpers", { + x1 <- rep(TRUE, 10) + x2 <- rep(FALSE, 10) + expect_success(expect_all_true(x1)) + expect_success(expect_all_false(x2)) +})