Skip to content

Commit 5b773d5

Browse files
committed
Rough in challenging-tests index
1 parent 84ed956 commit 5b773d5

File tree

1 file changed

+77
-35
lines changed

1 file changed

+77
-35
lines changed

vignettes/challenging-tests.Rmd

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,42 +20,78 @@ snapper$start_file("snapshotting.Rmd", "test")
2020
Sys.setenv(TESTTHAT_PKG = "testthat")
2121
```
2222

23-
Index to tools to solving challenging problems, organised by the problem, not the technique used to solve it. Designed to short so you can skim the whole thing, spot the problem your facing, and then see the tools you can use to solve it.
23+
This vignette is a quick reference guide for testing challenging functions. It's organised by the problem, rather than technique used to solve it, so you can quickly skim the whole vignette, spot the problem your facing, then learn more about useful tools for solving it.
2424

2525
## Options and environment variables
2626

27-
In this case can temporarily override with withr functions:
27+
If your function depends on options or environment variables, first try refactoring the functions to make the [inputs explicit](https://design.tidyverse.org/inputs-explicit.html). If that's not possible, then you can use a function like `withr::local_options()` or `withr::local_envvar()` to temporarily change options and environment values within a test. Learn more in `vignette("test-fixtures")`.
2828

29-
* Temporarily change options with `withr::local_options()`.
30-
* Temporarily change env vars with `withr::local_envvar()`.
29+
## Random numbers
3130

32-
`vignette("test-fixtures")`
31+
Random number generators generate different numbers each time you can them because they update a special `.Random.seed` variable. You can temporarily set this seed to a known value to make your random numbers reproducible with `withr::local_seed()`, making random numbers a special case of test fixtures.
3332

34-
## HTTP requests
33+
```{r}
34+
#| label: random-local-seed
35+
36+
dice <- function() {
37+
sample(6, 1)
38+
}
3539
36-
* If you need to test HTTP requests, we recommend using {vcr} or {httptest2}.
40+
test_that("dice returns different numbers", {
41+
withr::local_seed(1234)
3742
38-
## User interaction
43+
expect_equal(dice(), 4)
44+
expect_equal(dice(), 2)
45+
expect_equal(dice(), 6)
46+
})
47+
```
48+
49+
Alternatively, you might want to mock your function that wraps a random number generator:
50+
51+
```{r}
52+
#| label: random-mock
53+
54+
roll_three <- function() {
55+
sum(dice(), dice(), dice())
56+
}
57+
58+
test_that("three dice adds values of individual calls", {
59+
local_mocked_bindings(dice = mock_output_sequence(1, 2, 3))
60+
expect_equal(roll_three(), 6)
61+
})
62+
```
63+
64+
## HTTP requests
3965

40-
Mock away the function that the user returns.
66+
If you're trying to test functions that rely on HTTP requests, we recommend using {vcr} or {httptest2}. These packages provide the ability to record and then later reply HTTP requests so that you can test without an active internet connection. If your package is going to CRAN, we highly recommend either using one of these packages or using `skip_on_cran()` for your internet facing tests. This ensures that your package won't break on CRAN just because the service you're using is temporarily down.
4167

42-
`vignette("mocking")`
68+
## User interaction
4369

44-
For example, if your code uses `readline` to get feedback from the user, you can mock it. This is a good place to `mock_output_sequence()` if you want to simulate the user answering multiple questions.
70+
If you're testing a function that relies on user feedback from `readline()` or `menu()` or similar, you can use mocking (`vignette("mocking")`) to temporarily make those functions return fixed values. For example, imagine that you have the following function that asks the user if they want to continue:
4571

4672
```{r}
47-
readline <- NULL
73+
#| label: continue
4874
49-
continue <- function() {
50-
repeat{
51-
val <- readline("Do you want to continue? (y/n) ")
52-
if (val %in% c("y", "n")) {
53-
return(val == "y")
54-
}
55-
cat("! You must enter y or n\n")
75+
continue <- function(prompt) {
76+
cat(prompt, "\n", sep = "")
77+
78+
repeat {
79+
val <- readline("Do you want to continue? (y/n) ")
80+
if (val %in% c("y", "n")) {
81+
return(val == "y")
82+
}
83+
cat("! You must enter y or n\n")
5684
}
5785
}
5886
87+
readline <- NULL
88+
```
89+
90+
You can test its behaviour by mocking `readline()` and using a snapshot test:
91+
92+
```{r}
93+
#| label: mock-readline
94+
5995
test_that("user must respond y or n", {
6096
mock_readline <- local({
6197
i <- 0
@@ -69,35 +105,41 @@ test_that("user must respond y or n", {
69105
})
70106
71107
local_mocked_bindings(readline = mock_readline)
72-
expect_snapshot(val <- continue())
108+
expect_snapshot(val <- continue("This is dangerous"))
73109
expect_true(val)
74110
})
75111
```
76112

77-
## Random numbers
78-
79-
Random number generation is a special case of test fixures (`vignette("test-fixtures")`) 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()`.
113+
If you were testing the behaviour of some function that used `continue()`, you might choose to mock it directly:
80114

81115
```{r}
82-
dice <- function() {
83-
sample(6, 1)
116+
#| label: mock-continue
117+
118+
save_file <- function(path, data) {
119+
if (file.exists(path)) {
120+
if (!continue("`path` already exists")) {
121+
stop("Failed to continue")
122+
}
123+
}
124+
writeLines(data, path)
84125
}
85126
86-
test_that("dice returns different numbers", {
87-
withr::local_seed(1234)
127+
test_that("save_file() requires confirmation to overwrite file", {
128+
path <- withr::local_tempfile(lines = letters)
88129
89-
expect_equal(dice(), 4)
90-
expect_equal(dice(), 2)
91-
expect_equal(dice(), 6)
130+
local_mocked_bindings(continue = function(...) TRUE)
131+
save_file(path, "a")
132+
expect_equal(readLines(path), "a")
133+
134+
local_mocked_bindings(continue = function(...) FALSE)
135+
expect_snapshot(save_file(path, "a"), error = TRUE)
92136
})
93137
```
94138

95-
## Errors
96-
97-
Errors, 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.
139+
## User-facing text
98140

99-
Test with snapshot tests: `vignette("snapshotting")`.
141+
Errors, 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; learn more in `vignette("snapshotting")`.
100142

101143
## Testing interfaces
102144

103-
Sometimes you want to ensure multiple functions obey the same interface (e.g. they have the same arguments, or all error in the same way). You can use nested tests for this: see `vignette("nested-tests")`.
145+
Sometimes you want to ensure multiple functions obey the same interface (e.g. they have the same arguments, or all error in the same way). In this case you can write a helper function that calls `test_that()` and then use this inside your other tests. This works because testthat (as of 3.3.0) supports nested tests, which you can learn more about in `vignette("nested-tests")`.

0 commit comments

Comments
 (0)