Skip to content

Commit 24a6b53

Browse files
authored
Implement expect_no_warnings() and friends (#1690)
Fixes #1679
1 parent 3ca1157 commit 24a6b53

9 files changed

+285
-3
lines changed

NAMESPACE

+4
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ export(expect_match)
102102
export(expect_message)
103103
export(expect_more_than)
104104
export(expect_named)
105+
export(expect_no_condition)
106+
export(expect_no_error)
105107
export(expect_no_match)
108+
export(expect_no_message)
109+
export(expect_no_warning)
106110
export(expect_null)
107111
export(expect_output)
108112
export(expect_output_file)

NEWS.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# testthat (development version)
22

3+
* New experimental `expect_no_error()`, `expect_no_warning()`,
4+
`expect_no_message()`, and `expect_no_condition()` for asserting
5+
the code runs without an error, warning, message, or condition (#1679).
6+
37
* Fixed a warning in R >=4.2.0 on Windows that occurred when using the C++
48
testing infrastructure that testthat provides (#1672).
59

R/expect-condition.R

+11-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@
4242
#' error message.
4343
#' * If `NULL`, the default, asserts that there should be an error,
4444
#' but doesn't test for a specific value.
45-
#' * If `NA`, asserts that there should be no errors.
45+
#' * If `NA`, asserts that there should be no errors, but we now recommend
46+
#' using [expect_no_error()] and friends instead.
47+
#'
48+
#' Note that you should only use `message` with errors/warnings/messages
49+
#' that you generate. Avoid tests that rely on the specific text generated by
50+
#' another package since this can easily change. If you do need to test text
51+
#' generated by another package, either protect the test with `skip_on_cran()`
52+
#' or use `expect_snapshot()`.
4653
#' @inheritDotParams expect_match -object -regexp -info -label -all
4754
#' @param class Instead of supplying a regular expression, you can also supply
4855
#' a class name. This is useful for "classed" conditions.
@@ -51,6 +58,9 @@
5158
#' @param all *DEPRECATED* If you need to test multiple warnings/messages
5259
#' you now need to use multiple calls to `expect_message()`/
5360
#' `expect_warning()`
61+
#' @seealso [expect_no_error()], `expect_no_warning()`,
62+
#' `expect_no_message()`, and `expect_no_condition()` to assert
63+
#' that code runs without errors/warnings/messages/conditions.
5464
#' @return If `regexp = NA`, the value of the first argument; otherwise
5565
#' the captured condition.
5666
#' @examples

R/expect-no-condition.R

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#' Does code run without error, warning, message, or other condition?
2+
#'
3+
#' @description
4+
#' `r lifecycle::badge("experimental")`
5+
#'
6+
#' These expectations are the opposite of [expect_error()],
7+
#' `expect_warning()`, `expect_message()`, and `expect_condition()`. They
8+
#' assert the absence of an error, warning, or message, respectively.
9+
#'
10+
#' @inheritParams expect_error
11+
#' @param message,class The default, `message = NULL, class = NULL`,
12+
#' will fail if there is any error/warning/message/condition.
13+
#'
14+
#' If many cases, particularly when testing warnings and message, you will
15+
#' want to be more specific about the condition you are hoping **not** to see,
16+
#' i.e. the condition that motivated you to write the test. Similar to
17+
#' `expect_error()` and friends, you can specify the `message` (a regular
18+
#' expression that the message of the condition must match) and/or the
19+
#' `class` (a class the condition must inherit from). This ensures that
20+
#' the message/warnings you don't want never recur, while allowing new
21+
#' messages/warnings to bubble up for you to deal with.
22+
#'
23+
#' Note that you should only use `message` with errors/warnings/messages
24+
#' that you generate, or that base R generates (which tend to be stable).
25+
#' Avoid tests that rely on the specific text generated by another package
26+
#' since this can easily change. If you do need to test text generated by
27+
#' another package, either protect the test with `skip_on_cran()` or
28+
#' use `expect_snapshot()`.
29+
#' @inheritParams rlang::args_dots_empty
30+
#' @export
31+
#' @examples
32+
#' expect_no_warning(1 + 1)
33+
#'
34+
#' foo <- function(x) {
35+
#' warning("This is a problem!")
36+
#' }
37+
#'
38+
#' # warning doesn't match so bubbles up:
39+
#' expect_no_warning(foo(), message = "bananas")
40+
#'
41+
#' # warning does match so causes a failure:
42+
#' try(expect_no_warning(foo(), message = "problem"))
43+
expect_no_error <- function(object,
44+
...,
45+
message = NULL,
46+
class = NULL) {
47+
check_dots_empty()
48+
expect_no_("error", {{ object }}, ..., regexp = message, class = class)
49+
}
50+
51+
52+
#' @export
53+
#' @rdname expect_no_error
54+
expect_no_warning <- function(object,
55+
...,
56+
message = NULL,
57+
class = NULL
58+
) {
59+
check_dots_empty()
60+
expect_no_("warning", {{ object }}, ..., regexp = message, class = class)
61+
}
62+
63+
#' @export
64+
#' @rdname expect_no_error
65+
expect_no_message <- function(object,
66+
...,
67+
message = NULL,
68+
class = NULL
69+
) {
70+
check_dots_empty()
71+
expect_no_("messsage", {{ object }}, ..., regexp = message, class = class)
72+
}
73+
74+
#' @export
75+
#' @rdname expect_no_error
76+
expect_no_condition <- function(object,
77+
...,
78+
message = NULL,
79+
class = NULL
80+
) {
81+
check_dots_empty()
82+
expect_no_("condition", {{ object }}, ..., regexp = message, class = class)
83+
}
84+
85+
86+
expect_no_ <- function(base_class,
87+
object,
88+
regexp = NULL,
89+
class = NULL,
90+
...,
91+
error_call = caller_env()) {
92+
93+
check_dots_used(action = warn, call = error_call)
94+
matcher <- cnd_matcher(class %||% base_class, regexp, ...)
95+
96+
capture <- function(code) {
97+
try_fetch(
98+
code,
99+
!!base_class := function(cnd) {
100+
if (!matcher(cnd)) {
101+
return(zap())
102+
}
103+
104+
expected <- paste0(
105+
"Expected ", quo_label(enquo(object)), " to run without any ", base_class, "s",
106+
if (!is.null(class)) paste0(" of class '", class, "'"),
107+
if (!is.null(regexp)) paste0(" matching pattern '", regexp, "'"),
108+
"."
109+
)
110+
actual <- paste0(
111+
"Actually got a <", class(cnd)[[1]], ">:\n",
112+
indent_lines(rlang::cnd_message(cnd, prefix = TRUE))
113+
)
114+
message <- format_error_bullets(c(expected, i = actual))
115+
fail(message, trace_env = error_call)
116+
}
117+
)
118+
}
119+
120+
act <- quasi_capture(enquo(object), NULL, capture)
121+
succeed()
122+
invisible(act$val)
123+
}
124+
125+
indent_lines <- function(x) {
126+
paste0(" ", gsub("\n", "\n ", x))
127+
}

_pkgdown.yml

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ reference:
3434
- subtitle: Side-effects
3535
contents:
3636
- expect_error
37+
- expect_no_error
3738
- expect_invisible
3839
- expect_output
3940
- expect_silent

man/expect_error.Rd

+13-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/expect_no_error.Rd

+64
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# matched conditions give informative message
2+
3+
Code
4+
expect_no_warning(foo())
5+
Condition
6+
Error:
7+
! Expected `foo()` to run without any warnings.
8+
i Actually got a <test>:
9+
Warning:
10+
This is a problem!
11+
Code
12+
expect_no_warning(foo(), message = "problem")
13+
Condition
14+
Error:
15+
! Expected `foo()` to run without any warnings matching pattern 'problem'.
16+
i Actually got a <test>:
17+
Warning:
18+
This is a problem!
19+
Code
20+
expect_no_warning(foo(), class = "test")
21+
Condition
22+
Error:
23+
! Expected `foo()` to run without any warnings of class 'test'.
24+
i Actually got a <test>:
25+
Warning:
26+
This is a problem!
27+
Code
28+
expect_no_warning(foo(), message = "problem", class = "test")
29+
Condition
30+
Error:
31+
! Expected `foo()` to run without any warnings of class 'test' matching pattern 'problem'.
32+
i Actually got a <test>:
33+
Warning:
34+
This is a problem!
35+
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
test_that("unmatched conditions bubble up", {
2+
expect_error(expect_no_error(abort("foo"), message = "bar"), "foo")
3+
expect_warning(expect_no_warning(warn("foo"), message = "bar"), "foo")
4+
expect_message(expect_no_message(inform("foo"), message = "bar"), "foo")
5+
expect_condition(expect_no_condition(signal("foo", "x"), message = "bar"), "foo")
6+
})
7+
8+
test_that("only matches conditions of specified type", {
9+
foo <- function() {
10+
warn("This is a problem!", class = "test")
11+
}
12+
expect_warning(expect_no_error(foo(), class = "test"), class = "test")
13+
})
14+
15+
test_that("matched conditions give informative message", {
16+
foo <- function() {
17+
warn("This is a problem!", class = "test")
18+
}
19+
20+
expect_snapshot(error = TRUE, {
21+
expect_no_warning(foo())
22+
expect_no_warning(foo(), message = "problem")
23+
expect_no_warning(foo(), class = "test")
24+
expect_no_warning(foo(), message = "problem", class = "test")
25+
})
26+
})

0 commit comments

Comments
 (0)