Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# testthat (development version)

* `expect_no_failures()` and `expect_no_successes()` are now deprecated as `expect_success()` now test for no failures and `expect_failure()` tests for no successes (#)
* New `pass()` function to use in place of `succeed()` (#2113).
* `expectation()` is now a combination of `new_expectation()` and `exp_signal()` (#2125).
* `is_null()`/`matches()` deprecated in 2.0.0 (2017-12-19) and `is_true()`/`is_false()` deprecated in 2.1.0 (2019-04-23) have been removed (#2109).
Expand Down
135 changes: 90 additions & 45 deletions R/expect-self-test.R
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
capture_failure <- new_capture("expectation_failure")
capture_success <- function(expr) {
capture_success_failure <- function(expr) {
cnd <- NULL

withCallingHandlers(
expr,
expectation_failure = function(cnd) {
invokeRestart("continue_test")
},
expectation_success = function(cnd) {
cnd <<- cnd
}
n_success <- 0
n_failure <- 0

last_failure <- NULL

withRestarts(
withCallingHandlers(
expr,
expectation_failure = function(cnd) {
last_failure <<- cnd
n_failure <<- n_failure + 1
# Finish the test without bubbling up
invokeRestart("failed")
Comment on lines +7 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC n_failure is either 0 or 1, because we're unwinding back to failed at the first failure?

In this case I think it would be a little clearer to remove n_failure and rename last_failure (whose "last" prefix imply there might be more than one assigned here) to just failure. Then you test for !is.null(failure) instead of n_failure > 0.

Also fine as is but then I'd add a comment saying we expect at most 1 failure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good suggestion but I'm going to leave it until after #2129 because there I am encountering a weird situation where fail() is not causing an expectation to exit and I think that this might help me debug it.

},
expectation_success = function(cnd) {
n_success <<- n_success + 1
# Don't bubble up to any other handlers
invokeRestart("continue_test")
}
),
failed = function() {}
)
cnd
}

new_capture("expectation_success")
list(
n_success = n_success,
n_failure = n_failure,
last_failure = last_failure
)
}

#' Tools for testing expectations
#'
#' @description
#' * `expect_success()` and `expect_failure()` check that there's at least
#' one success or failure respectively.
#' * `expect_snapshot_failure()` records the failure message so that you can
#' manually check that it is informative.
#' * `expect_no_success()` and `expect_no_failure()` check that are no
#' successes or failures.
#' `expect_success()` checks that there's exactly one success and no failures;
#' `expect_failure()` checks that there's exactly one failure and no successes.
#' `expect_snapshot_failure()` records the failure message so that you can
#' manually check that it is informative.
#'
#' Use `show_failure()` in examples to print the failure message without
#' throwing an error.
Expand All @@ -34,42 +47,50 @@ new_capture("expectation_success")
#' @param ... Other arguments passed on to [expect_match()].
#' @export
expect_success <- function(expr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems likely to be a breaking change. I guess you have a system for revdep checking before release, but it might be worth trying out on a sample of 50/100 packages now to see how big the impact might be.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at https://github.com/search?q=org%3Acran+expect_success+path%3A%2F%5Etests%5C%2Ftestthat%5C%2F%2F&type=code suggests that there will be a few packages that this breaks, but most of the uses of expect_success() look incorrect and think PRs that just remove expect_success() shouldn't be too hard for me to do.

exp <- capture_success(expr)
status <- capture_success_failure(expr)

if (is.null(exp)) {
if (status$n_success == 0) {
fail("Expectation did not succeed")
} else {
succeed()
} else if (status$n_success > 1) {
fail(sprintf(
"Expectation succeeded %i times, instead of once",
status$n_success
))
}
invisible(NULL)
}

#' @export
#' @rdname expect_success
expect_no_success <- function(expr) {
exp <- capture_success(expr)

if (!is.null(exp)) {
fail("Expectation succeeded")
} else {
succeed()
if (status$n_failure > 0) {
fail(sprintf(
"Expectation failed %i times, instead of zero",
status$n_failure
))
}
invisible(NULL)

pass(NULL)
}

#' @export
#' @rdname expect_success
expect_failure <- function(expr, message = NULL, ...) {
exp <- capture_failure(expr)
status <- capture_success_failure(expr)

if (is.null(exp)) {
if (status$n_failure == 0) {
fail("Expectation did not fail")
} else if (!is.null(message)) {
expect_match(exp$message, message, ...)
} else {
succeed()
} else if (status$n_failure > 1) {
# This should be impossible, but including for completeness
fail("Expectation failed more than once")
Comment on lines +78 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah then you wouldn't need this branch

}
invisible(NULL)

if (status$n_success != 0) {
fail(sprintf(
"Expectation succeeded %i times, instead of never",
status$n_success
))
}

if (!is.null(message)) {
return(expect_match(status$last_failure$message, message, ...))
}
pass(NULL)
}

#' @export
Expand All @@ -78,12 +99,36 @@ expect_snapshot_failure <- function(expr) {
expect_snapshot_error(expr, "expectation_failure")
}

#' Test for absence of success or failure
#'
#' @description
#' `r lifecycle::badge("deprecated")`
#'
#' These functions are deprecated because [expect_success()] and
#' [expect_failure()] now test for exactly one success or no failures, and
#' exactly one failure and no successes.
#'
#' @keywords internal
#' @export
#' @rdname expect_success
expect_no_success <- function(expr) {
lifecycle::deprecate_warn("3.3.0", "expect_no_success()", "expect_failure()")
status <- capture_success_failure(expr)

if (status$n_success > 0) {
fail("Expectation succeeded")
} else {
succeed()
}
invisible(NULL)
}

#' @export
#' @rdname expect_no_success
expect_no_failure <- function(expr) {
exp <- capture_failure(expr)
lifecycle::deprecate_warn("3.3.0", "expect_no_failure()", "expect_success()")
status <- capture_success_failure(expr)

if (!is.null(exp)) {
if (status$n_failure > 0) {
fail("Expectation failed")
} else {
succeed()
Expand Down
2 changes: 1 addition & 1 deletion R/try-again.R
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ try_again <- function(times, code) {
times <- times - 1L
}

stop(e)
exp_signal(e)
}
19 changes: 19 additions & 0 deletions man/expect_no_success.Rd

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

16 changes: 3 additions & 13 deletions man/expect_success.Rd

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

23 changes: 23 additions & 0 deletions tests/testthat/_snaps/expect-self-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# errors in expect_success bubble up

Code
expect_success(abort("error"))
Condition
Error:
! error

# expect_no are deprecated

Code
expect_no_failure(succeed())
Condition
Warning:
`expect_no_failure()` was deprecated in testthat 3.3.0.
i Please use `expect_success()` instead.
Code
expect_no_success(fail())
Condition
Warning:
`expect_no_success()` was deprecated in testthat 3.3.0.
i Please use `expect_failure()` instead.

13 changes: 11 additions & 2 deletions tests/testthat/test-expect-condition.R
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,17 @@ test_that("can capture Throwable conditions from rJava", {

test_that("capture correct trace_env (#1994)", {
# This should fail, not error
expect_failure(expect_error(stop("oops")) |> expect_warning())
expect_failure(expect_warning(expect_error(stop("oops"))))
status <- capture_success_failure({
stop("oops") |> expect_error() |> expect_warning()
})
expect_equal(status$n_success, 1) # from expect_error()
expect_equal(status$n_failure, 1) # from expect_warning()

status <- capture_success_failure({
stop("oops") %>% expect_error() %>% expect_warning()
})
expect_equal(status$n_success, 1) # from expect_error()
expect_equal(status$n_failure, 1) # from expect_warning()
})

# expect_warning() ----------------------------------------------------------
Expand Down
27 changes: 9 additions & 18 deletions tests/testthat/test-expect-known.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ test_that("uses specified width", {

test_that("creates file on first run", {
file <- withr::local_tempfile()
expect_success(
expect_warning(
expect_known_output(cat("ok!\n"), file),
"Creating reference"
)
)
expect_known_output(cat("ok!\n"), file) |>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the 4.1.0 requirement is so satisfying 🧘‍♂️

Copy link
Member Author

@hadley hadley Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simple joys of being able to use R features that were released 5 years ago 😆

expect_success() |>
expect_warning("Creating reference")

expect_true(file.exists(file))
})
Expand Down Expand Up @@ -76,12 +73,9 @@ test_that("correctly matches to a file", {
})

test_that("first run is successful", {
expect_success(
expect_warning(
expect_known_value(2, "two.rds"),
"Creating reference"
)
)
expect_known_value(2, "two.rds") |>
expect_success() |>
expect_warning("Creating reference")
unlink("two.rds")
})

Expand Down Expand Up @@ -130,12 +124,9 @@ test_that("version 3 is possible", {
# expect_known_hash -------------------------------------------------------

test_that("empty hash succeeds with warning", {
expect_success(
expect_warning(
expect_known_hash(1:10),
"No recorded hash"
)
)
expect_known_hash(1:10) |>
expect_success() |>
expect_warning("No recorded hash")
})

test_that("only succeeds if hash is correct", {
Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test-expect-no-condition.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test_that("expect_no_* pass with pure code", {
})

test_that("expect_no_* don't emit success when they fail", {
expect_no_success(expect_no_error(stop("!")))
expect_failure(expect_no_error(stop("!")))
})

test_that("capture correct trace_env (#1994)", {
Expand Down
Loading
Loading