You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
18
+
19
+
Other packages:
20
+
21
+
* For testing graphical output, we recommend vdiffr.
22
+
* For testing code that uses HTTP requests we recommend vcr or httptest2.
23
+
17
24
```{r setup}
18
25
library(testthat)
19
26
```
20
27
21
-
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.
28
+
## External state
22
29
23
-
## Output Affected by RNG
30
+
Tests should be isolated from global options, environment variables, and other external state that might affect behavior.
24
31
25
-
Random number generation can make tests non-deterministic. Use `withr::local_seed()` to ensure reproducible results within your tests.
32
+
### Output affected by RNG
26
33
27
-
### The Problem
34
+
Random number generation can make tests non-deterministic. Use `withr::local_seed()` to ensure reproducible results within your tests.
28
35
29
36
```{r, eval = FALSE}
30
-
# This test will randomly pass or fail
31
-
test_that("random sample has expected properties", {
@@ -108,51 +94,31 @@ test_that("get_api_url uses default when env var not set", {
108
94
})
109
95
```
110
96
111
-
### Working Directory
97
+
### Reading and writing files
112
98
113
99
```{r}
114
100
test_that("function works in different directories", {
115
-
withr::local_dir(tempdir())
101
+
withr::local_dir(withr::local_tempdir())
116
102
# Test code that depends on working directory
117
103
writeLines("test content", "temp_file.txt")
118
104
expect_true(file.exists("temp_file.txt"))
119
105
# File will be cleaned up automatically
120
106
})
121
107
```
122
108
123
-
## Graphical Output
124
-
125
-
Testing plots and other graphical output requires specialized tools. The [vdiffr](https://vdiffr.r-lib.org/) package provides visual regression testing for ggplot2 and base R graphics.
126
-
127
-
### Setting Up vdiffr
109
+
### Local wrappers
128
110
129
-
```{r, eval = FALSE}
130
-
# In your test file
131
-
library(vdiffr)
132
-
133
-
test_that("plot looks correct", {
134
-
p <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
135
-
expect_doppelganger("basic scatterplot", p)
136
-
})
137
-
```
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()`.
The first time you run these tests, vdiffr will create reference images. Subsequent runs will compare against these references and flag any visual differences.
150
-
151
-
## Errors and User-Facing Text
117
+
## Errors and user-facing text
152
118
153
119
Error messages, warnings, and other user-facing text should be tested to ensure they're helpful and consistent. Snapshots are perfect for this.
@@ -198,59 +149,45 @@ test_that("summarize_data output is correct", {
198
149
})
199
150
```
200
151
201
-
## HTTP Responses
152
+
The same idea applies to messages and warnings.
202
153
203
-
Testing code that makes HTTP requests requires mocking to avoid external dependencies. Use httr2 mocking for httr2-based code, or httptest2 for httr-based code.
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.
# httptest2 will look for mock files in tests/testthat/api.example.com/
243
-
result <- get_user_info(123)
244
-
expect_equal(result$id, 123)
245
-
})
246
-
})
247
-
```
166
+
* Package versions and installed status
167
+
* Retrieving external state (vcr typically best) but sometimes better at higher level. e.g. token prices in ellemr.
168
+
* Pretending that you're on a different operating system
169
+
* Cause things to deliberately error
170
+
* The passing of time
171
+
* Slow functions that aren't important for specific test
172
+
* 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.
But we generally recommend using `rlang::is_interactive()`. Can be manually overridden by `rlang_interactive` option, whih is automatically set inside of tests.
254
191
255
192
```{r}
256
193
ask_yes_no <- function(question) {
@@ -269,31 +206,10 @@ test_that("ask_yes_no handles no response", {
269
206
})
270
207
```
271
208
272
-
### Mocking File Selection
273
209
274
-
```{r}
275
-
read_user_file <- function() {
276
-
file_path <- file.choose()
277
-
readLines(file_path)
278
-
}
210
+
## Reducing duplication
279
211
280
-
test_that("read_user_file works with mocked file selection", {
1.**Isolate tests**: Use `withr` functions to ensure tests don't affect each other
381
-
2.**Make tests deterministic**: Control randomness with seeds
382
-
3.**Test the interface**: Focus on testing user-facing behavior, not implementation details
383
-
4.**Use appropriate tools**: Choose the right mocking/testing approach for your specific challenge
384
-
5.**Document complex setups**: Add comments explaining why specific mocking or setup is needed
385
-
6.**Keep tests fast**: Mock external dependencies to avoid network calls and file I/O when possible
386
-
387
-
By addressing these challenging scenarios systematically, you can build confidence that your code works correctly under all conditions your users might encounter.
0 commit comments