Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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_succeses()` are now deprecated as `expect_success()` now test for no failures and `expect_failure()` tests for no successes (#)
* `local_edition()` now gives a useful error for bad values (#1547).
* testthat now requires R 4.1.
* `expect_s4_class()` now supports unquoting (@stibu81, #2064).
Expand Down
131 changes: 88 additions & 43 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,56 +47,88 @@ 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)) {
fail("Expectation did not succeed")
} else {
if (status$n_success == 1 && status$n_failure == 0) {
succeed()
} else if (status$n_success == 0) {
fail("Expectation did not succeed")
} else if (status$n_success > 1) {
fail(sprintf(
"Expectation succeeded %i times, instead of once",
status$n_success
))
} else if (status$n_failure > 0) {
fail(sprintf(
"Expectation failed %i times, instead of zero",
status$n_failure
))
}

invisible(NULL)
}

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

if (!is.null(exp)) {
fail("Expectation succeeded")
} else {
if (status$n_failure == 1 && status$n_success == 0) {
if (!is.null(message)) {
return(expect_match(status$last_failure$message, message, ...))
}
succeed()
} else if (status$n_failure == 0) {
fail("Expectation did not fail")
} 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

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

invisible(NULL)
}

#' @export
#' @rdname expect_success
expect_failure <- function(expr, message = NULL, ...) {
exp <- capture_failure(expr)
expect_snapshot_failure <- function(expr) {
expect_snapshot_error(expr, "expectation_failure")
}

if (is.null(exp)) {
fail("Expectation did not fail")
} else if (!is.null(message)) {
expect_match(exp$message, message, ...)
#' 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
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_success
expect_snapshot_failure <- function(expr) {
expect_snapshot_error(expr, "expectation_failure")
}

#' @export
#' @rdname expect_success
#' @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