Skip to content

Commit 551f101

Browse files
authored
Refine describe() and it() (#2165)
Introducing a stack of test descriptions and moving reporter creation to `test_code()` allows `describe()` and `it()` to be much simpler and much more similar. I've also bulked out the testing a little more, since it was pretty basic previously. Fixes #2007
1 parent 39dc80d commit 551f101

File tree

14 files changed

+110
-81
lines changed

14 files changed

+110
-81
lines changed

.claude/settings.local.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
],
1010
"deny": []
1111
}
12-
}
12+
}

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+
* `describe()`, `it()`, and `test_that()` now have a shared stack of descriptions so that if you nest any inside of each other, any resulting failures will show you the full path.
4+
* `describe()` now correctly scopes `skip()` (#2007).
35
* `ParallelProgressReporter` now respect `max_failures` (#1162).
46
* The last snapshot is no longer lost if the snapshot file is missing the final newline (#2092). It's easy to accidentally remove this because there are two trailing new lines in snapshot files and many editors will automatically remove if you touch the file.
57
* New `expect_r6_class()` (#2030).

R/describe.R

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,43 +56,17 @@
5656
#' it("can handle division by 0") #not yet implemented
5757
#' })
5858
#' })
59-
6059
describe <- function(description, code) {
61-
check_string(description, allow_empty = FALSE)
62-
describe_description <- description
63-
64-
# prepares a new environment for each it-block
65-
describe_environment <- new.env(parent = parent.frame())
66-
describe_environment$it <- function(description, code = NULL) {
67-
check_string(description, allow_empty = FALSE)
68-
code <- substitute(code)
69-
70-
description <- paste0(describe_description, ": ", description)
71-
describe_it(description, code, describe_environment)
72-
}
73-
74-
eval(substitute(code), describe_environment)
75-
invisible()
76-
}
77-
78-
describe_it <- function(description, code, env = parent.frame()) {
79-
reporter <- get_reporter() %||% local_interactive_reporter()
80-
local_test_context()
60+
local_description_push(description)
8161

82-
test_code(
83-
description,
84-
code,
85-
env = env,
86-
reporter = reporter,
87-
skip_on_empty = FALSE
88-
)
62+
test_code(code, parent.frame(), skip_on_empty = FALSE)
8963
}
9064

9165
#' @export
9266
#' @rdname describe
9367
it <- function(description, code = NULL) {
94-
check_string(description, allow_empty = FALSE)
68+
local_description_push(description)
9569

9670
code <- substitute(code)
97-
describe_it(description, code, env = parent.frame())
71+
test_code(code, env = parent.frame(), skip_on_empty = FALSE)
9872
}

R/reporter-silent.R

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ SilentReporter <- R6::R6Class(
2828
}
2929
)
3030
)
31+
32+
# Useful for testing test_that() and friends which otherwise swallow
33+
# all expectations by design
34+
capture_expectations <- function(code) {
35+
reporter <- SilentReporter$new()
36+
with_reporter(reporter, code)
37+
reporter$expectations()
38+
}

R/reporter-zzz.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ get_reporter <- function() {
3636
#' @rdname reporter-accessors
3737
#' @export
3838
with_reporter <- function(reporter, code, start_end_reporter = TRUE) {
39+
# Ensure we don't propagate the local description to the new reporter
40+
local_description_set()
3941
reporter <- find_reporter(reporter)
4042

4143
old <- set_reporter(reporter)

R/snapshot-reporter.R

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ SnapshotReporter <- R6::R6Class(
3535
},
3636

3737
start_test = function(context, test) {
38-
if (is.character(test)) {
39-
self$test <- gsub("\n", "", test)
38+
if (is.null(test)) {
39+
return()
4040
}
41+
42+
self$test <- paste0(gsub("\n", "", test), collapse = " / ")
4143
},
4244

4345
# Called by expectation

R/source.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ source_file <- function(
5555
withr::local_options(testthat_topenv = env, testthat_path = path)
5656
if (wrap) {
5757
invisible(test_code(
58-
test = NULL,
5958
code = exprs,
6059
env = env,
6160
reporter = get_reporter() %||% StopReporter$new()

R/test-example.R

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,23 @@ test_examples_installed <- function(package = testing_package()) {
4747
#' @export
4848
#' @rdname test_examples
4949
test_rd <- function(rd, title = attr(rd, "Rdfile")) {
50-
test_example(rd, title)
50+
test_example(rd, title %||% "example")
5151
}
5252

5353
#' @export
5454
#' @rdname test_examples
5555
test_example <- function(path, title = path) {
56+
local_description_push(title)
57+
5658
ex_path <- withr::local_tempfile(pattern = "test_example-", fileext = ".R")
5759
tools::Rd2ex(path, ex_path)
5860
if (!file.exists(ex_path)) {
5961
return(invisible(FALSE))
6062
}
6163

62-
env <- new.env(parent = globalenv())
63-
6464
ok <- test_code(
65-
test = title,
6665
code = parse(ex_path, encoding = "UTF-8"),
67-
env = env,
66+
env = globalenv(),
6867
reporter = get_reporter() %||% StopReporter$new(),
6968
skip_on_empty = FALSE
7069
)

R/test-that.R

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
#' })
3535
#' }
3636
test_that <- function(desc, code) {
37-
check_string(desc)
37+
local_description_push(desc)
3838

3939
code <- substitute(code)
4040
if (edition_get() >= 3) {
@@ -46,23 +46,19 @@ test_that <- function(desc, code) {
4646
}
4747
}
4848

49-
# Must initialise interactive reporter before local_test_context()
50-
reporter <- get_reporter() %||% local_interactive_reporter()
51-
local_test_context()
52-
53-
test_code(
54-
desc,
55-
code,
56-
env = parent.frame(),
57-
reporter = reporter
58-
)
49+
test_code(code, env = parent.frame())
5950
}
6051

6152
# Access error fields with `[[` rather than `$` because the
6253
# `$.Throwable` from the rJava package throws with unknown fields
63-
test_code <- function(test, code, env, reporter, skip_on_empty = TRUE) {
54+
test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
55+
# Must initialise interactive reporter before local_test_context()
56+
reporter <- get_reporter() %||% local_interactive_reporter()
57+
local_test_context()
58+
6459
frame <- caller_env()
6560

61+
test <- test_description()
6662
if (!is.null(test)) {
6763
reporter$start_test(context = reporter$.context, test = test)
6864
withr::defer(reporter$end_test(context = reporter$.context, test = test))
@@ -192,3 +188,30 @@ test_code <- function(test, code, env, reporter, skip_on_empty = TRUE) {
192188

193189
invisible(ok)
194190
}
191+
192+
193+
# Maintain a stack of descriptions
194+
local_description_push <- function(description, frame = caller_env()) {
195+
check_string(description, call = frame)
196+
local_description_set(c(the$description, description), frame = frame)
197+
}
198+
local_description_set <- function(
199+
description = character(),
200+
frame = caller_env()
201+
) {
202+
check_character(description, call = frame)
203+
204+
old <- the$description
205+
the$description <- description
206+
withr::defer(the$description <- old, frame)
207+
208+
invisible(old)
209+
}
210+
211+
test_description <- function() {
212+
if (length(the$description) == 0) {
213+
NULL
214+
} else {
215+
paste(the$description, collapse = " / ")
216+
}
217+
}

R/testthat-package.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
NULL
2020

2121
the <- new.env(parent = emptyenv())
22-
22+
the$description <- character()
2323

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

0 commit comments

Comments
 (0)