Skip to content

Commit 8258144

Browse files
committed
More writing
1 parent 3d37462 commit 8258144

File tree

2 files changed

+103
-90
lines changed

2 files changed

+103
-90
lines changed

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
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.
88

9-
## Key Development Commands
9+
## Key development commands
1010

1111
General advice:
1212
* When running R from the console, always run it with `--quiet --vanilla`
@@ -23,7 +23,8 @@ General advice:
2323

2424
### Documentation
2525

26-
- Always run `devtools::document()` after changing any roxygen2 docs.
26+
- Run `devtools::document()` after changing any roxygen2 docs.
27+
- Use sentence case for all headings
2728

2829
## Core Architecture
2930

vignettes/challenging-tests.Rmd

Lines changed: 100 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -14,111 +14,105 @@ knitr::opts_chunk$set(
1414
)
1515
```
1616

17-
Testing is easy when your functions are pure: they take some inputs and return predictable outputs. But real-world code often involves randomness, external state, graphics, user interaction, and other challenging elements. This vignette provides practical solutions for testing these tricky scenarios.
17+
Testing is easy when your functions are pure: they take some inputs and return predictable outputs. But real-world code often involves randomness, external state, graphics, user interaction, and other challenging elements. This vignette provides practical solutions these tricky scenarios.
1818

19-
Other packages:
19+
In principle, it's often possible to test these things by explicitly parameterising them as arguments to your functions so you can more easily override the default values. And where possible you should do so, especially when testing internal functions. But it's often impractical to provide arguments to explicitly control every last feature without exploding user-facing interfaces. So the techniques in this vignette will help you test all your code, regardless of where it lives and what it does.
2020

21-
* For testing graphical output, we recommend vdiffr.
22-
* For testing code that uses HTTP requests we recommend vcr or httptest2.
21+
This vignette is divided into sections based on the underlying tool you'll use:
2322

24-
```{r setup}
25-
library(testthat)
26-
```
23+
* External state shows you how to use the withr package to handle options, environment variables, the working direction, and random number generation.
24+
* Snapshotting shows you how to handle functions that produce user facing output including text, warnings, and errors.
25+
* Mocking is general purpose tool when all else fails; it allows you to temporarily replace a function or method with a mockup that you can control.
26+
* Subtests shows you how to use functions and for-loops to reduce duplication in your test code, making it easier to test that multiple part of your package have the same behaviour or follow the same interface.
2727

28-
## External state
28+
To begin, there are a couple of scenarios that testthat doesn't help with, but we can happily suggest other suggest:
2929

30-
Tests should be isolated from global options, environment variables, and other external state that might affect behavior.
30+
* If you need to test graphical output, {vdiffr}. vdiffr is used to test ggplot2, and incorporates everything we know about high-quality graphics tests that minimise false positives.
3131

32-
### Output affected by RNG
32+
* If you need to test HTTP requests, we recommend using {vcr} or {httptest2}.
3333

34-
Random number generation can make tests non-deterministic. Use `withr::local_seed()` to ensure reproducible results within your tests.
34+
```{r setup}
35+
library(testthat)
36+
```
3537

36-
```{r, eval = FALSE}
37-
simulate_data <- function(n) {
38-
rnorm(n, mean = 0, sd = 1)
39-
}
38+
## External state (withr)
4039

41-
test_that("simulate_data returns correct structure", {
42-
result <- simulate_data(5)
43-
expect_length(result, 5)
44-
expect_type(result, "double")
45-
expect_equal(result[1], 1.048, tolerance = 0.001)
46-
})
47-
```
40+
### Options, env vars, and working directory
4841

49-
```{r}
50-
test_that("random sample has expected properties", {
51-
withr::local_seed(123)
52-
x <- sample(1:100, 10)
53-
expect_length(x, 10)
54-
expect_true(all(x %in% 1:100))
55-
# This will always pass now:
56-
expect_equal(x[1], 31)
57-
})
58-
```
42+
If your code depends on global options, environment variables, or the working directory. In most cases, it's good practice to make these dependencies explicit by making them the default value of an argument so you can control directly in your tests. However, sometimes you are testing deeply embedded code and it would be painful to thread the values all the way through to the right place. In this case can temporarily override with withr functions:
5943

60-
### Global options
44+
* Temporarily change options with `withr::local_options()`.
45+
* Temporarily change env vars with `withr::local_envvar()`.
46+
* Temporarily change the working directory with `withr::local_dir()`.
6147

6248
```{r}
63-
# Function that depends on global options
6449
format_number <- function(x) {
6550
format(x, digits = getOption("digits"))
6651
}
6752
6853
test_that("format_number respects digits option", {
69-
# Save and restore the original option
54+
x <- 1.23456
7055
withr::local_options(digits = 3)
71-
expect_equal(format_number(pi), "3.14")
56+
expect_equal(format_number(x), "1.23")
7257
7358
withr::local_options(digits = 5)
74-
expect_equal(format_number(pi), "3.1416")
59+
expect_equal(format_number(x), "1.2346")
7560
})
7661
```
7762

78-
### Environment variables
63+
### Random numbers
64+
65+
Random number generation also falls into the same bucket because it depends on the value of the special `.Random.seed` variable which is updated whenever you generate a random number. You can temporarily change this seed and reproducibly generate "random" numbers with `withr::local_seed()`.
7966

8067
```{r}
81-
# Function that depends on environment variables
82-
get_api_url <- function() {
83-
Sys.getenv("API_URL", default = "https://api.example.com")
68+
dice <- function() {
69+
sample(6, 1)
8470
}
8571
86-
test_that("get_api_url uses environment variable", {
87-
withr::local_envvar(API_URL = "https://test-api.example.com")
88-
expect_equal(get_api_url(), "https://test-api.example.com")
89-
})
72+
test_that("dice returns different numbers", {
73+
withr::local_seed(1234)
9074
91-
test_that("get_api_url uses default when env var not set", {
92-
withr::local_envvar(API_URL = NA)
93-
expect_equal(get_api_url(), "https://api.example.com")
75+
expect_equal(dice(), 4)
76+
expect_equal(dice(), 2)
77+
expect_equal(dice(), 6)
9478
})
9579
```
9680

97-
### Reading and writing files
81+
### Local helpers
82+
83+
If you find yourself using the same `local_` calls in multiple places, you may want to create your own helper function. This is straightforward once you know how these functions. The most important thing to know is that they are all wrappers around `on.exit()` which runs code when a function exits. The question is: which function? By default, it's the function that calls `withr::local_*()`. But obvious that's not going to work if you write a helper function:
9884

9985
```{r}
100-
test_that("function works in different directories", {
101-
withr::local_dir(withr::local_tempdir())
102-
# Test code that depends on working directory
103-
writeLines("test content", "temp_file.txt")
104-
expect_true(file.exists("temp_file.txt"))
105-
# File will be cleaned up automatically
86+
local_my_helper <- function() {
87+
withr::local_options(x = 10)
88+
}
89+
90+
local({
91+
local_my_helper()
92+
getOption("x")
10693
})
10794
```
10895

109-
### Local wrappers
110-
111-
If you want to make your own function, you should take a `frame` argument. frame is an environment on the call stack, i.e. it's the execution environment of some function, and the local effects will be undone when that function is completed. Underneath the hood this is all wrappers around `on.exit()`.
96+
To resolve this problem we need to capture the calling frame for our helper function. A **frame** is an environment on the call stack, i.e. the execution environment of some function that lead to the current call.
11297

11398
```{r}
99+
local_my_helper <- function(frame = parent.frame()) {
100+
withr::local_options(x = 10, .local_envir = frame)
101+
}
102+
local({
103+
local_my_helper()
104+
getOption("x")
105+
})
114106
115107
```
116108

109+
We strongly recommend giving such functions a `local_` prefix to clearly communicate that they have "local" effects.
110+
117111
## Errors and user-facing text
118112

119-
Error messages, warnings, and other user-facing text should be tested to ensure they're helpful and consistent. Snapshots are perfect for this.
113+
Error messages, warnings, and other user-facing text should be tested to ensure they're helpful and consistent. Obviously you can't test this 100% automatically, but you can ensure that such messaging is clearly shown in PRs, so another human can take a look. This is exactly the point of snapshot tests.
120114

121-
### Testing error messages
115+
Snapshot tets are particularly important when testing complex error messages.
122116

123117
```{r}
124118
divide_positive <- function(x, y) {
@@ -134,6 +128,32 @@ test_that("divide_positive gives helpful error", {
134128
})
135129
```
136130

131+
### Complex error messages with cli
132+
133+
When generating sophisticated error messages that use cli's interpolation and formatting features, snapshot tests are essential for ensuring the messages render correctly with proper styling and content.
134+
135+
```{r}
136+
process_order <- function(item_count, price, discount_code, shipping_country) {
137+
if (item_count <= 0 || price <= 0 || nchar(shipping_country) != 2) {
138+
cli::cli_abort(c(
139+
"Invalid order parameters:",
140+
"x" = "Item count: {.val {item_count}} (must be > 0)",
141+
"x" = "Price: {.val {price}} (must be > 0)",
142+
"x" = "Shipping country: {.val {shipping_country}} (must be 2-letter code)",
143+
"x" = "Discount code: {.val {discount_code}} ({length(discount_code)} character{?s})",
144+
"i" = "Order processing failed for {.pkg {Sys.info()[['user']]}} at {.timestamp {Sys.time()}}"
145+
))
146+
}
147+
148+
list(items = item_count, total = price, country = shipping_country)
149+
}
150+
151+
test_that("process_order shows complex cli interpolated errors", {
152+
expect_snapshot_error(process_order(0, -10, "INVALID", "USA"))
153+
expect_snapshot_error(process_order(-5, 25.99, "", "X"))
154+
})
155+
```
156+
137157
### Testing complex output
138158

139159
```{r}
@@ -153,9 +173,15 @@ The same idea applies to messages and warnings.
153173

154174
### `local_reproducible_output()`
155175

176+
By default, testthat sets a number of options that simplify and standardise output:
156177

178+
* The console width is set to 80.
179+
* Crayon/cli ANSI colouring and hyperlinks are suppressed.
180+
* Unicode characters are suppressed.
157181

158-
### Transformations
182+
These are sound defaults that we have found useful to minimise spurious diffs between tests run in different environment. But it's sometimes necessary to override them in order to test various output features. So, if necessary, you can override these settings by calling `local_reproducible_output()`. Read its docs to learn more.
183+
184+
### Transforms
159185

160186
Sometimes part of the output varies in ways that you can't easily control. There are two techniques you can use: mocking (described next) or the `transform` output.
161187

@@ -172,42 +198,28 @@ Sometimes part of the output varies in ways that you can't easily control. There
172198
* Sometimes easier or more clear to mock a function rather than setting options/env vars. And generally just tickling some branch that would otherwise be hard to reach.
173199
* Record internal state with `<<-`.
174200

175-
```{r}
176-
unix_time <- function() unclass(Sys.time())
177-
178-
time <- 0
179-
local_mocked_bindings(unix_time = function(time) time)
180-
time <- 1
181-
time <- 10
182-
```
183-
184-
### Interactivity and user input
201+
### Managing time
185202

186203
```{r}
187-
local_mocked_bindings(interactive = function() FALSE)
204+
#| eval: false
205+
unix_time <- function() unclass(Sys.time())
206+
207+
time <- 0
208+
local_mocked_bindings(unix_time = function(time) time)
209+
time <- 1
210+
time <- 10
188211
```
189212

190-
But we generally recommend using `rlang::is_interactive()`. Can be manually overridden by `rlang_interactive` option, whih is automatically set inside of tests.
213+
### Interactivity and user input
191214

192215
```{r}
193-
ask_yes_no <- function(question) {
194-
response <- readline(paste0(question, " (y/n): "))
195-
tolower(response) %in% c("y", "yes")
196-
}
197-
198-
test_that("ask_yes_no handles yes response", {
199-
mockery::stub(ask_yes_no, "readline", "y")
200-
expect_true(ask_yes_no("Continue?"))
201-
})
202-
203-
test_that("ask_yes_no handles no response", {
204-
mockery::stub(ask_yes_no, "readline", "n")
205-
expect_false(ask_yes_no("Continue?"))
206-
})
216+
#| eval: false
217+
local_mocked_bindings(interactive = function() FALSE)
207218
```
208219

220+
But we generally recommend using `rlang::is_interactive()`. Can be manually overridden by `rlang_interactive` option, whih is automatically set inside of tests.More
209221

210-
## Reducing duplication
222+
## Subtests
211223

212224
### Using helper functions
213225

0 commit comments

Comments
 (0)