Skip to content

Commit 2e911f4

Browse files
authored
Reliable nested subtests (#2193)
A few problems here: * `describe()` forgot to `substitute()` code. * `StopReporter` was called at the end of every (sub)test instead of just once. This also reqiured changes to `local_interactive_reporter()` * `StopReporter()` doesn't need the call in its error * In `test_code()` needs to accumulate expectations across its children * Dropped `test_code()` braces check since it no longer seems needed and makes all the functions exactly the same. Fixes #2063. Fixes #2188.
1 parent 596b3e4 commit 2e911f4

File tree

11 files changed

+60
-43
lines changed

11 files changed

+60
-43
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# testthat (development version)
22

3+
* `test_that()` no longer warns about the absence of `{}` since it no longer seems to be necessary.
4+
* `test_that()`, `describe()`, and `it()` can now be arbitrarily nested. Each component will skip only if it and its subtests don't contain any expectations. The interactive stop reporter has been fixed so it doesn't duplicate failures. (#2063, #2188).
35
* Test filtering now works with `it()`, and the `desc` argument can take a character vector in order to recursively filter subtests (i.e. `it()` nested inside of `describe()`) (#2118).
46
* New `snapshot_reject()` rejects all modified snapshots by deleting the `.new` variants (#1923).
57
* New `SlowReporter` makes it easier to find the slowest tests in your package. The easiest way to run it is with `devtools::test(reporter = "slow")` (#1466).

R/describe.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
describe <- function(description, code) {
6161
local_description_push(description)
6262

63-
test_code(code, parent.frame(), skip_on_empty = FALSE)
63+
code <- substitute(code)
64+
test_code(code, parent.frame())
6465
}
6566

6667
#' @export
@@ -69,5 +70,5 @@ it <- function(description, code = NULL) {
6970
local_description_push(description)
7071

7172
code <- substitute(code)
72-
test_code(code, env = parent.frame(), skip_on_empty = FALSE)
73+
test_code(code, parent.frame())
7374
}

R/local.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ local_interactive_reporter <- function(.env = parent.frame()) {
200200
# Use StopReporter
201201
reporter <- StopReporter$new()
202202
old <- set_reporter(reporter)
203+
withr::defer(reporter$end_reporter(), envir = .env)
203204
withr::defer(reporter$stop_if_needed(), envir = .env)
204205
withr::defer(set_reporter(old), envir = .env)
205206

R/reporter-stop.R

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,25 @@ StopReporter <- R6::R6Class(
4545
self$n_fail <- self$n_fail + 1
4646
}
4747
self$issues$push(result)
48+
49+
self$local_user_output()
50+
self$cat_line(issue_summary(result, rule = TRUE), "\n")
4851
},
4952

50-
end_test = function(context, test) {
53+
end_reporter = function(context, test) {
5154
self$local_user_output()
5255

5356
if (self$issues$size() == 0) {
5457
if (self$praise && self$n_success > 0) {
5558
emoji <- praise_emoji()
5659
self$cat_line(colourise("Test passed", "success"), " ", emoji)
5760
}
58-
} else {
59-
issues <- self$issues$as_list()
60-
messages <- map_chr(issues, issue_summary, rule = TRUE)
61-
self$cat_line(messages, "\n")
6261
}
6362
},
6463

6564
stop_if_needed = function() {
6665
if (self$stop_reporter && self$n_fail > 0) {
67-
cli::cli_abort("Test failed.")
66+
cli::cli_abort("Test failed.", call = NULL)
6867
}
6968
}
7069
)

R/test-that.R

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,7 @@ test_that <- function(desc, code) {
3737
local_description_push(desc)
3838

3939
code <- substitute(code)
40-
if (edition_get() >= 3) {
41-
if (!is_call(code, "{")) {
42-
cli::cli_warn(
43-
"The {.arg code} argument to {.fn test_that} must be a braced expression to get accurate file-line information for failures.",
44-
class = "testthat_braces_warning"
45-
)
46-
}
47-
}
48-
49-
test_code(code, env = parent.frame())
40+
test_code(code, parent.frame())
5041
}
5142

5243
# Access error fields with `[[` rather than `$` because the
@@ -64,6 +55,15 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
6455
withr::defer(reporter$end_test(context = reporter$.context, test = test))
6556
}
6657

58+
if (the$top_level_test) {
59+
# Not strictly necessary but nice to reset the count
60+
the$test_expectations <- 0
61+
the$top_level_test <- FALSE
62+
withr::defer(the$top_level_test <- TRUE)
63+
}
64+
# Used to skip if the test _and_ its subtests have no expectations
65+
starting_expectations <- the$test_expectations
66+
6767
ok <- TRUE
6868

6969
# @param debug_end How many frames should be skipped to find the
@@ -88,13 +88,8 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
8888
expressions_opt <- getOption("expressions")
8989
expressions_opt_new <- min(expressions_opt + 500L, 500000L)
9090

91-
# If no handlers are called we skip: BDD (`describe()`) tests are often
92-
# nested and the top level might not contain any expectations, so we need
93-
# some way to disable
94-
handled <- !skip_on_empty
95-
9691
handle_error <- function(e) {
97-
handled <<- TRUE
92+
the$test_expectations <- the$test_expectations + 1L
9893

9994
# Increase option(expressions) to handle errors here if possible, even in
10095
# case of a stack overflow. This is important for the DebugReporter.
@@ -109,11 +104,11 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
109104
invokeRestart("end_test")
110105
}
111106
handle_fatal <- function(e) {
112-
handled <<- TRUE
107+
the$test_expectations <- the$test_expectations + 1L
113108
register_expectation(e, 0)
114109
}
115110
handle_expectation <- function(e) {
116-
handled <<- TRUE
111+
the$test_expectations <- the$test_expectations + 1L
117112
register_expectation(e, 7)
118113
# Don't bubble up to any other handlers
119114
invokeRestart("continue_test")
@@ -143,7 +138,7 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
143138
}
144139
}
145140
handle_skip <- function(e) {
146-
handled <<- TRUE
141+
the$test_expectations <- the$test_expectations + 1L
147142

148143
debug_end <- if (inherits(e, "skip_empty")) -1 else 2
149144
register_expectation(e, debug_end)
@@ -168,7 +163,8 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
168163
withCallingHandlers(
169164
{
170165
eval(code, test_env)
171-
if (!handled && !is.null(test)) {
166+
new_expectations <- the$test_expectations > starting_expectations
167+
if (!new_expectations && skip_on_empty) {
172168
skip_empty()
173169
}
174170
},

R/testthat-package.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ NULL
2020

2121
the <- new.env(parent = emptyenv())
2222
the$description <- character()
23+
the$top_level_test <- TRUE
24+
the$test_expectations <- 0
2325

2426
# The following block is used by usethis to automatically manage
2527
# roxygen namespace tags. Modify with care!

man/source_file.Rd

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/reporter-stop.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# produces useful output
22

3-
Test passed
43
-- Failure ('reporters/tests.R:12:3'): Failure:1 -------------------------------
54
FALSE (`actual`) is not equal to TRUE (`expected`).
65

@@ -53,6 +52,6 @@
5352
Code
5453
r$stop_if_needed()
5554
Condition
56-
Error in `r$stop_if_needed()`:
55+
Error:
5756
! Test failed.
5857

tests/testthat/test-describe.R

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ describe("describe", {
1111
})
1212
})
1313
})
14+
})
1415

15-
it("can have not yet implemented specs", {
16-
describe("Millennium Prize Problems", {
17-
it("can be shown that P != NP")
16+
test_that("unimplemented specs generate skips", {
17+
expectations <- capture_expectations({
18+
it("can have not yet implemented specs", {
19+
describe("Millennium Prize Problems", {
20+
it("can be shown that P != NP")
21+
})
1822
})
1923
})
24+
expect_length(expectations, 1)
25+
expect_s3_class(expectations[[1]], "expectation_skip")
2026
})
2127

2228
someExternalVariable <- 1

tests/testthat/test-snapshot-manage.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test_that("can work with variants", {
3636

3737
test_that("snapshot_reject deletes .new files", {
3838
path <- local_snapshot_dir(c("a.md", "a.new.md", "b.md", "b.new.md"))
39-
39+
4040
expect_snapshot(snapshot_reject(path = path))
4141
expect_equal(dir(file.path(path, "_snaps")), c("a.md", "b.md"))
4242
})

0 commit comments

Comments
 (0)