Skip to content

Commit 2ae77ff

Browse files
committed
First pass from claude
1 parent 7eb22e8 commit 2ae77ff

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed

vignettes/challenging-tests.Rmd

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
---
2+
title: "Challenging Testing Problems"
3+
output: rmarkdown::html_vignette
4+
vignette: >
5+
%\VignetteIndexEntry{Challenging Testing Problems}
6+
%\VignetteEngine{knitr::rmarkdown}
7+
%\VignetteEncoding{UTF-8}
8+
---
9+
10+
```{r, include = FALSE}
11+
knitr::opts_chunk$set(
12+
collapse = TRUE,
13+
comment = "#>"
14+
)
15+
```
16+
17+
```{r setup}
18+
library(testthat)
19+
```
20+
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.
22+
23+
## Output Affected by RNG
24+
25+
Random number generation can make tests non-deterministic. Use `withr::local_seed()` to ensure reproducible results within your tests.
26+
27+
### The Problem
28+
29+
```{r, eval = FALSE}
30+
# This test will randomly pass or fail
31+
test_that("random sample has expected properties", {
32+
x <- sample(1:100, 10)
33+
expect_length(x, 10)
34+
expect_true(all(x %in% 1:100))
35+
# This might fail randomly:
36+
expect_equal(x[1], 42)
37+
})
38+
```
39+
40+
### The Solution
41+
42+
```{r}
43+
test_that("random sample has expected properties", {
44+
withr::local_seed(123)
45+
x <- sample(1:100, 10)
46+
expect_length(x, 10)
47+
expect_true(all(x %in% 1:100))
48+
# This will always pass now:
49+
expect_equal(x[1], 31)
50+
})
51+
```
52+
53+
For functions that internally use random numbers:
54+
55+
```{r}
56+
simulate_data <- function(n) {
57+
rnorm(n, mean = 0, sd = 1)
58+
}
59+
60+
test_that("simulate_data returns correct structure", {
61+
withr::local_seed(456)
62+
result <- simulate_data(5)
63+
expect_length(result, 5)
64+
expect_type(result, "double")
65+
# Test specific values with fixed seed
66+
expect_equal(result[1], 1.048, tolerance = 0.001)
67+
})
68+
```
69+
70+
## Output Affected by External State
71+
72+
Tests should be isolated from global options, environment variables, and other external state that might affect behavior.
73+
74+
### Global Options
75+
76+
```{r}
77+
# Function that depends on global options
78+
format_number <- function(x) {
79+
format(x, digits = getOption("digits"))
80+
}
81+
82+
test_that("format_number respects digits option", {
83+
# Save and restore the original option
84+
withr::local_options(digits = 3)
85+
expect_equal(format_number(pi), "3.14")
86+
87+
withr::local_options(digits = 5)
88+
expect_equal(format_number(pi), "3.1416")
89+
})
90+
```
91+
92+
### Environment Variables
93+
94+
```{r}
95+
# Function that depends on environment variables
96+
get_api_url <- function() {
97+
Sys.getenv("API_URL", default = "https://api.example.com")
98+
}
99+
100+
test_that("get_api_url uses environment variable", {
101+
withr::local_envvar(API_URL = "https://test-api.example.com")
102+
expect_equal(get_api_url(), "https://test-api.example.com")
103+
})
104+
105+
test_that("get_api_url uses default when env var not set", {
106+
withr::local_envvar(API_URL = NA)
107+
expect_equal(get_api_url(), "https://api.example.com")
108+
})
109+
```
110+
111+
### Working Directory
112+
113+
```{r}
114+
test_that("function works in different directories", {
115+
withr::local_dir(tempdir())
116+
# Test code that depends on working directory
117+
writeLines("test content", "temp_file.txt")
118+
expect_true(file.exists("temp_file.txt"))
119+
# File will be cleaned up automatically
120+
})
121+
```
122+
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
128+
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+
```
138+
139+
### Base R Graphics
140+
141+
```{r, eval = FALSE}
142+
test_that("base R plot is correct", {
143+
expect_doppelganger("base histogram", function() {
144+
hist(rnorm(100), main = "Normal Distribution")
145+
})
146+
})
147+
```
148+
149+
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
152+
153+
Error messages, warnings, and other user-facing text should be tested to ensure they're helpful and consistent. Snapshots are perfect for this.
154+
155+
### Testing Error Messages
156+
157+
```{r}
158+
divide_positive <- function(x, y) {
159+
if (y <= 0) {
160+
stop("Divisor must be positive, got: ", y)
161+
}
162+
x / y
163+
}
164+
165+
test_that("divide_positive gives helpful error", {
166+
expect_snapshot_error(divide_positive(10, -2))
167+
expect_snapshot_error(divide_positive(10, 0))
168+
})
169+
```
170+
171+
### Testing Warnings
172+
173+
```{r}
174+
maybe_warn <- function(x) {
175+
if (x < 0) {
176+
warning("Negative value detected: ", x)
177+
}
178+
abs(x)
179+
}
180+
181+
test_that("maybe_warn produces expected warning", {
182+
expect_snapshot(maybe_warn(-5))
183+
})
184+
```
185+
186+
### Testing Complex Output
187+
188+
```{r}
189+
summarize_data <- function(x) {
190+
cat("Summary of data:\n")
191+
cat("Length:", length(x), "\n")
192+
cat("Mean:", mean(x), "\n")
193+
cat("Range:", range(x), "\n")
194+
}
195+
196+
test_that("summarize_data output is correct", {
197+
expect_snapshot(summarize_data(1:10))
198+
})
199+
```
200+
201+
## HTTP Responses
202+
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.
204+
205+
### With httr2
206+
207+
```{r, eval = FALSE}
208+
library(httr2)
209+
210+
get_user_info <- function(user_id) {
211+
req <- request("https://api.example.com") |>
212+
req_url_path_append("users", user_id)
213+
resp <- req_perform(req)
214+
resp_body_json(resp)
215+
}
216+
217+
test_that("get_user_info handles successful response", {
218+
# Mock the HTTP response
219+
with_mocked_responses(
220+
request("https://api.example.com/users/123") |>
221+
req_method("GET") |>
222+
mock_response(
223+
status_code = 200,
224+
body = '{"id": 123, "name": "Alice"}'
225+
),
226+
{
227+
result <- get_user_info(123)
228+
expect_equal(result$id, 123)
229+
expect_equal(result$name, "Alice")
230+
}
231+
)
232+
})
233+
```
234+
235+
### With httptest2
236+
237+
```{r, eval = FALSE}
238+
library(httptest2)
239+
240+
test_that("API call works", {
241+
with_mock_api({
242+
# 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+
```
248+
249+
## Interactivity
250+
251+
Interactive functions that prompt for user input need mocking to work in automated tests.
252+
253+
### Mocking User Input
254+
255+
```{r}
256+
ask_yes_no <- function(question) {
257+
response <- readline(paste0(question, " (y/n): "))
258+
tolower(response) %in% c("y", "yes")
259+
}
260+
261+
test_that("ask_yes_no handles yes response", {
262+
mockery::stub(ask_yes_no, "readline", "y")
263+
expect_true(ask_yes_no("Continue?"))
264+
})
265+
266+
test_that("ask_yes_no handles no response", {
267+
mockery::stub(ask_yes_no, "readline", "n")
268+
expect_false(ask_yes_no("Continue?"))
269+
})
270+
```
271+
272+
### Mocking File Selection
273+
274+
```{r}
275+
read_user_file <- function() {
276+
file_path <- file.choose()
277+
readLines(file_path)
278+
}
279+
280+
test_that("read_user_file works with mocked file selection", {
281+
temp_file <- tempfile()
282+
writeLines(c("line 1", "line 2"), temp_file)
283+
284+
mockery::stub(read_user_file, "file.choose", temp_file)
285+
result <- read_user_file()
286+
287+
expect_equal(result, c("line 1", "line 2"))
288+
unlink(temp_file)
289+
})
290+
```
291+
292+
## Testing Many Combinations
293+
294+
When you need to test many parameter combinations, use helper functions and loops to avoid repetitive code.
295+
296+
### Using Helper Functions
297+
298+
```{r}
299+
# Function to test
300+
power_function <- function(x, n) {
301+
if (n < 0) stop("Negative exponents not supported")
302+
if (x == 0 && n == 0) stop("0^0 is undefined")
303+
x^n
304+
}
305+
306+
# Helper function for testing
307+
test_power <- function(x, n, expected) {
308+
test_that(paste0("power_function(", x, ", ", n, ") equals ", expected), {
309+
expect_equal(power_function(x, n), expected)
310+
})
311+
}
312+
313+
# Test many combinations
314+
test_power(2, 3, 8)
315+
test_power(5, 2, 25)
316+
test_power(10, 0, 1)
317+
test_power(-3, 2, 9)
318+
```
319+
320+
### Using Loops for Systematic Testing
321+
322+
```{r}
323+
test_that("power_function works for multiple bases and exponents", {
324+
test_cases <- data.frame(
325+
x = c(2, 3, 4, 5),
326+
n = c(2, 2, 2, 2),
327+
expected = c(4, 9, 16, 25)
328+
)
329+
330+
for (i in seq_len(nrow(test_cases))) {
331+
expect_equal(
332+
power_function(test_cases$x[i], test_cases$n[i]),
333+
test_cases$expected[i],
334+
info = paste("Failed for x =", test_cases$x[i], "n =", test_cases$n[i])
335+
)
336+
}
337+
})
338+
```
339+
340+
### Property-Based Testing
341+
342+
```{r}
343+
test_that("power_function satisfies mathematical properties", {
344+
# Test that x^0 = 1 for any non-zero x
345+
for (x in c(-10, -1, 1, 2, 10, 100)) {
346+
expect_equal(power_function(x, 0), 1,
347+
info = paste("x^0 should equal 1 for x =", x))
348+
}
349+
350+
# Test that x^1 = x for any x
351+
for (x in c(-5, 0, 1, 7, 100)) {
352+
expect_equal(power_function(x, 1), x,
353+
info = paste("x^1 should equal x for x =", x))
354+
}
355+
})
356+
```
357+
358+
### Testing Edge Cases Systematically
359+
360+
```{r}
361+
test_that("power_function handles edge cases correctly", {
362+
# Test error conditions
363+
error_cases <- list(
364+
list(x = 5, n = -1, pattern = "Negative exponents"),
365+
list(x = 0, n = 0, pattern = "0\\^0 is undefined")
366+
)
367+
368+
for (case in error_cases) {
369+
expect_error(
370+
power_function(case$x, case$n),
371+
case$pattern,
372+
info = paste("Expected error for x =", case$x, "n =", case$n)
373+
)
374+
}
375+
})
376+
```
377+
378+
## Best Practices
379+
380+
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

Comments
 (0)