Skip to content

Commit e4ab555

Browse files
authored
Use pass() and fail() in testthat expectations (#2129)
Instead of using `expect()`, we now prefer early returns with `fail()` and a final return with `pass()`. The main thing I (re?) discovered here is that you must return `fail()`, as while it throws interactively, it returns inside of `test_that()`.
1 parent ce394a5 commit e4ab555

38 files changed

+433
-384
lines changed

.Rbuildignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@
2323
^[\.]?air\.toml$
2424
^\.vscode$
2525
^\.git-blame-ignore-rev$
26+
^CLAUDE\.md$
27+
^\.claude$

.claude/settings.local.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(find:*)"
5+
],
6+
"deny": []
7+
}
8+
}

CLAUDE.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## About This Project
6+
7+
testthat is R's most popular unit testing framework, used by thousands of CRAN packages. It provides functions to make testing R code as fun and addictive as possible, with clear expectations, visual progress indicators, and seamless integration with R package development workflows.
8+
9+
## Key Development Commands
10+
11+
### Testing
12+
- `devtools::test()` or `Ctrl/Cmd+Shift+T` in RStudio - Run all tests
13+
- `devtools::test_file("tests/testthat/test-filename.R")` - Run tests in a specific file
14+
- `testthat::test_local()` - Run tests for local source package
15+
- `testthat::test_package("testthat")` - Run tests for installed package
16+
- `R CMD check` - Full package check including tests
17+
18+
### Building and Installation
19+
- `devtools::load_all()` or `Ctrl/Cmd+Shift+L` - Load package for development
20+
- `devtools::document()` - Generate documentation
21+
- `devtools::check()` - Run R CMD check
22+
- `devtools::install()` - Install package locally
23+
24+
## Core Architecture
25+
26+
### Main Components
27+
28+
1. **Core Testing Functions** (`R/test-that.R`, `R/test-package.R`):
29+
- `test_that()` - The fundamental testing function
30+
- `test_local()`, `test_package()`, `test_check()` - Different ways to run test suites
31+
32+
2. **Expectations** (`R/expect-*.R`):
33+
- Modular expectation functions (equality, conditions, types, etc.)
34+
- Each expectation type has its own file following the pattern `expect-[type].R`
35+
36+
3. **Reporters** (`R/reporter*.R`):
37+
- Different output formats for test results
38+
- Object-oriented design with base `Reporter` class
39+
- Includes check, debug, progress, summary, JUnit, TAP formats
40+
41+
4. **Snapshot Testing** (`R/snapshot*.R`):
42+
- Value snapshots, file snapshots, output snapshots
43+
- Automatic management and comparison of expected outputs
44+
45+
5. **Parallel Testing** (`R/parallel*.R`):
46+
- Multi-core test execution
47+
- Configuration via `Config/testthat/parallel: true` in DESCRIPTION
48+
49+
6. **Mocking** (`R/mock*.R`, `R/mock2*.R`):
50+
- Function mocking capabilities
51+
- Both legacy (`mock.R`) and modern (`mock2*.R`) implementations
52+
53+
### Key Design Patterns
54+
55+
- **Editions**: testthat has different "editions" with varying behavior, controlled by `Config/testthat/edition`
56+
- **Reporters**: Extensible reporting system using R6 classes
57+
- **Lazy Evaluation**: Expectations use substitute() and lazy evaluation for better error messages
58+
- **C++ Integration**: Core functionality implemented in C++ for performance
59+
60+
### File Organization
61+
62+
- `R/` - All R source code, organized by functionality
63+
- `src/` - C++ source code and Makevars
64+
- `inst/include/testthat/` - C++ headers for other packages to use
65+
- `tests/testthat/` - Package's own comprehensive test suite
66+
- `vignettes/` - Documentation on testing concepts and workflows
67+
68+
### Important Configuration
69+
70+
The package uses several DESCRIPTION fields for configuration:
71+
- `Config/testthat/edition: 3` - Sets testthat edition
72+
- `Config/testthat/parallel: true` - Enables parallel testing
73+
- `Config/testthat/start-first` - Tests to run first in parallel mode
74+
75+
### C++ Testing Infrastructure
76+
77+
testthat provides C++ testing capabilities via Catch framework:
78+
- Headers in `inst/include/testthat/`
79+
- Test runner infrastructure in `src/test-runner.cpp`
80+
- Integration with R's testing system
81+
82+
### Snapshot Testing Workflow
83+
84+
- Snapshots stored in `tests/testthat/_snaps/`
85+
- Different snapshot types: values, files, output
86+
- Version-specific snapshots for different R versions
87+
- Use `testthat::snapshot_accept()` to update snapshots
88+
89+
This codebase prioritizes backward compatibility, comprehensive testing, and clear, descriptive error messages to help R developers write better tests.

R/expect-comparison.R

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,17 @@ expect_compare <- function(operator = c("<", "<=", ">", ">="), act, exp) {
3434
if (length(cmp) != 1 || !is.logical(cmp)) {
3535
abort("Result of comparison must be a single logical value")
3636
}
37-
expect(
38-
if (!is.na(cmp)) cmp else FALSE,
39-
sprintf(
37+
if (!isTRUE(cmp)) {
38+
msg <- sprintf(
4039
"%s is %s %s. Difference: %.3g",
4140
act$lab,
4241
msg,
4342
exp$lab,
4443
act$val - exp$val
45-
),
46-
trace_env = caller_env()
47-
)
48-
invisible(act$val)
44+
)
45+
return(fail(msg, trace_env = caller_env()))
46+
}
47+
pass(act$val)
4948
}
5049
#' @export
5150
#' @rdname comparison-expectations

R/expect-condition.R

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ expect_error <- function(
138138

139139
# Access error fields with `[[` rather than `$` because the
140140
# `$.Throwable` from the rJava package throws with unknown fields
141-
expect(is.null(msg), msg, info = info, trace = act$cap[["trace"]])
142-
invisible(act$val %||% act$cap)
141+
if (!is.null(msg)) {
142+
return(fail(msg, info = info, trace = act$cap[["trace"]]))
143+
}
144+
pass(act$val %||% act$cap)
143145
}
144146
}
145147

@@ -186,9 +188,10 @@ expect_warning <- function(
186188
...,
187189
cond_type = "warnings"
188190
)
189-
expect(is.null(msg), msg, info = info)
190-
191-
invisible(act$val)
191+
if (!is.null(msg)) {
192+
return(fail(msg, info = info))
193+
}
194+
pass(act$val)
192195
}
193196
}
194197

@@ -218,9 +221,10 @@ expect_message <- function(
218221
} else {
219222
act <- quasi_capture(enquo(object), label, capture_messages)
220223
msg <- compare_messages(act$cap, act$lab, regexp = regexp, all = all, ...)
221-
expect(is.null(msg), msg, info = info)
222-
223-
invisible(act$val)
224+
if (!is.null(msg)) {
225+
return(fail(msg, info = info))
226+
}
227+
pass(act$val)
224228
}
225229
}
226230

@@ -262,9 +266,10 @@ expect_condition <- function(
262266
inherit = inherit,
263267
cond_type = "condition"
264268
)
265-
expect(is.null(msg), msg, info = info, trace = act$cap[["trace"]])
266-
267-
invisible(act$val %||% act$cap)
269+
if (!is.null(msg)) {
270+
return(fail(msg, info = info, trace = act$cap[["trace"]]))
271+
}
272+
pass(act$val %||% act$cap)
268273
}
269274
}
270275

@@ -303,17 +308,12 @@ expect_condition_matching <- function(
303308

304309
# Access error fields with `[[` rather than `$` because the
305310
# `$.Throwable` from the rJava package throws with unknown fields
306-
expect(
307-
is.null(msg),
308-
msg,
309-
info = info,
310-
trace = act$cap[["trace"]],
311-
trace_env = trace_env
312-
)
313-
311+
if (!is.null(msg)) {
312+
return(fail(msg, info = info, trace = act$cap[["trace"]], trace_env = trace_env))
313+
}
314314
# If a condition was expected, return it. Otherwise return the value
315315
# of the expression.
316-
invisible(if (expected) act$cap else act$val)
316+
pass(if (expected) act$cap else act$val)
317317
}
318318

319319
# -------------------------------------------------------------------------

R/expect-constant.R

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ expect_false <- function(object, info = NULL, label = NULL) {
5656
#' show_failure(expect_null(y))
5757
expect_null <- function(object, info = NULL, label = NULL) {
5858
act <- quasi_label(enquo(object), label, arg = "object")
59-
6059
expect_waldo_constant(act, NULL, info = info)
6160
}
6261

@@ -71,17 +70,15 @@ expect_waldo_constant <- function(act, constant, info, ...) {
7170
...
7271
)
7372

74-
expect(
75-
length(comp) == 0,
76-
sprintf(
73+
if (length(comp) != 0) {
74+
msg <- sprintf(
7775
"%s is not %s\n\n%s",
7876
act$lab,
7977
deparse(constant),
8078
paste0(comp, collapse = "\n\n")
81-
),
82-
info = info,
83-
trace_env = caller_env()
84-
)
79+
)
80+
return(fail(msg, info = info, trace_env = caller_env()))
81+
}
8582

86-
invisible(act$val)
83+
pass(act$val)
8784
}

R/expect-equality.R

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,11 @@ expect_equal <- function(
7575
comp <- compare(act$val, exp$val, ...)
7676
}
7777

78-
expect(
79-
comp$equal,
80-
sprintf("%s not equal to %s.\n%s", act$lab, exp$lab, comp$message),
81-
info = info
82-
)
83-
invisible(act$val)
78+
if (!comp$equal) {
79+
msg <- sprintf("%s not equal to %s.\n%s", act$lab, exp$lab, comp$message)
80+
return(fail(msg, info = info))
81+
}
82+
pass(act$val)
8483
}
8584
}
8685

@@ -112,12 +111,11 @@ expect_identical <- function(
112111
}
113112
}
114113

115-
expect(
116-
ident,
117-
sprintf("%s not identical to %s.\n%s", act$lab, exp$lab, msg),
118-
info = info
119-
)
120-
invisible(act$val)
114+
if (!ident) {
115+
msg <- sprintf("%s not identical to %s.\n%s", act$lab, exp$lab, msg)
116+
return(fail(msg, info = info))
117+
}
118+
pass(act$val)
121119
}
122120
}
123121

@@ -129,22 +127,19 @@ expect_waldo_equal <- function(type, act, exp, info, ...) {
129127
x_arg = "actual",
130128
y_arg = "expected"
131129
)
132-
expect(
133-
length(comp) == 0,
134-
sprintf(
130+
if (length(comp) != 0) {
131+
msg <- sprintf(
135132
"%s (%s) not %s to %s (%s).\n\n%s",
136133
act$lab,
137134
"`actual`",
138135
type,
139136
exp$lab,
140137
"`expected`",
141138
paste0(comp, collapse = "\n\n")
142-
),
143-
info = info,
144-
trace_env = caller_env()
145-
)
146-
147-
invisible(act$val)
139+
)
140+
return(fail(msg, info = info, trace_env = caller_env()))
141+
}
142+
pass(act$val)
148143
}
149144

150145
#' Is an object equal to the expected value, ignoring attributes?
@@ -188,12 +183,16 @@ expect_equivalent <- function(
188183
)
189184

190185
comp <- compare(act$val, exp$val, ..., check.attributes = FALSE)
191-
expect(
192-
comp$equal,
193-
sprintf("%s not equivalent to %s.\n%s", act$lab, exp$lab, comp$message),
194-
info = info
195-
)
196-
invisible(act$val)
186+
if (!comp$equal) {
187+
msg <- sprintf(
188+
"%s not equivalent to %s.\n%s",
189+
act$lab,
190+
exp$lab,
191+
comp$message
192+
)
193+
return(fail(msg, info = info))
194+
}
195+
pass(act$val)
197196
}
198197

199198

@@ -225,12 +224,11 @@ expect_reference <- function(
225224
act <- quasi_label(enquo(object), label, arg = "object")
226225
exp <- quasi_label(enquo(expected), expected.label, arg = "expected")
227226

228-
expect(
229-
is_reference(act$val, exp$val),
230-
sprintf("%s not a reference to %s.", act$lab, exp$lab),
231-
info = info
232-
)
233-
invisible(act$val)
227+
if (!is_reference(act$val, exp$val)) {
228+
msg <- sprintf("%s not a reference to %s.", act$lab, exp$lab)
229+
return(fail(msg, info = info))
230+
}
231+
pass(act$val)
234232
}
235233

236234
# expect_reference() needs dev version of rlang

0 commit comments

Comments
 (0)