Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2ae77ff
First pass from claude
hadley Aug 9, 2025
5fcd5da
Roughing out ideas
hadley Aug 9, 2025
3d37462
Merged origin/main into challenging-tests
hadley Aug 13, 2025
8258144
More writing
hadley Aug 14, 2025
9366d4f
A bit more
hadley Aug 14, 2025
6d0ad9f
Still more words
hadley Aug 14, 2025
d64b395
Merge commit '6883737a71a0ee02d763123737a93fc70633bda2'
hadley Aug 28, 2025
84ed956
Break up into separate vignettes
hadley Aug 29, 2025
5b773d5
Rough in challenging-tests index
hadley Aug 29, 2025
ec50771
Mocking braindump
hadley Aug 29, 2025
b828a34
Merged origin/main into challenging-tests
hadley Aug 30, 2025
3dac615
Front load test fixtures vignette
hadley Sep 1, 2025
5279465
Revert accidental change
hadley Sep 1, 2025
029bc7a
More on mocking
hadley Sep 3, 2025
94a0196
Apply suggestions from code review
hadley Sep 3, 2025
98b3f47
Drop nested tests for now.
hadley Sep 3, 2025
6122a11
Snapshotting updates
hadley Sep 4, 2025
ca4c19e
Polish
hadley Sep 4, 2025
76ea3da
✨ Proofreading ✨
hadley Sep 4, 2025
4a091c4
Add section on skipping
hadley Sep 4, 2025
39ddd7c
✨ More proofreading ✨
hadley Sep 4, 2025
aa402f5
✨ More proofreading ✨
hadley Sep 4, 2025
d27dd24
✨ More proofreading ✨
hadley Sep 4, 2025
8a75d2d
✨ More proofreading ✨
hadley Sep 4, 2025
bc047bd
Changes based on claude pointing out problems
hadley Sep 4, 2025
2689b25
✨ More proofreading ✨
hadley Sep 4, 2025
7b3f603
Drop nested-tests from index
hadley Sep 4, 2025
e02bc05
More Jenny feedback
hadley Sep 4, 2025
c0b9f07
Add news bullet
hadley Sep 4, 2025
73eb396
Final human proofreading
hadley Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
^\.git-blame-ignore-rev$
^CLAUDE\.md$
^\.claude$
^vignettes/articles$
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

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.

## Key Development Commands
## Key development commands

General advice:
* When running R from the console, always run it with `--quiet --vanilla`
Expand All @@ -25,10 +25,11 @@ General advice:

### Documentation

- Always run `devtools::document()` after changing any roxygen2 docs.
- Run `devtools::document()` after changing any roxygen2 docs.
- Every user facing function should be exported and have roxygen2 documentation.
- Whenever you add a new documentation file, make sure to also add the topic name to `_pkgdown.yml`.
- Run `pkgdown::check_pkgdown()` to check that all topics are included in the reference index.
- Use sentence case for all headings

## Core Architecture

Expand Down
19 changes: 19 additions & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ reference:
- ends_with("Reporter")
- -Reporter

articles:
- title: Setup and configuration
navbar: Setup
contents:
- third-edition
- parallel
- special-files
- custom-expectation

- title: Testing techniques
navbar: Techniques
contents:
- challenging-tests
- mocking
- nested-tests
- skipping
- snapshotting
- test-fixtures

news:
releases:
- text: "Version 3.2.0"
Expand Down
145 changes: 145 additions & 0 deletions vignettes/challenging-tests.Rmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
title: "Testing challenging functions"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Testing challenging functions}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---

```{r}
#| include: false
library(testthat)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")

# Pretend we're snapshotting
snapper <- local_snapshotter(fail_on_new = FALSE)
snapper$start_file("snapshotting.Rmd", "test")

# Pretend we're testing testthat so we can use mocking
Sys.setenv(TESTTHAT_PKG = "testthat")
```

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.

## Options and environment variables

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")`.

## Random numbers

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.

```{r}
#| label: random-local-seed

dice <- function() {
sample(6, 1)
}

test_that("dice returns different numbers", {
withr::local_seed(1234)

expect_equal(dice(), 4)
expect_equal(dice(), 2)
expect_equal(dice(), 6)
})
```

Alternatively, you might want to mock your function that wraps a random number generator:

```{r}
#| label: random-mock

roll_three <- function() {
sum(dice(), dice(), dice())
}

test_that("three dice adds values of individual calls", {
local_mocked_bindings(dice = mock_output_sequence(1, 2, 3))
expect_equal(roll_three(), 6)
})
```

## HTTP requests

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.

## User interaction

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:

```{r}
#| label: continue

continue <- function(prompt) {
cat(prompt, "\n", sep = "")

repeat {
val <- readline("Do you want to continue? (y/n) ")
if (val %in% c("y", "n")) {
return(val == "y")
}
cat("! You must enter y or n\n")
}
}

readline <- NULL
```

You can test its behaviour by mocking `readline()` and using a snapshot test:

```{r}
#| label: mock-readline

test_that("user must respond y or n", {
mock_readline <- local({
i <- 0
function(prompt) {
i <<- i + 1
cat(prompt)
val <- if (i == 1) "x" else "y"
cat(val, "\n", sep = "")
val
}
})

local_mocked_bindings(readline = mock_readline)
expect_snapshot(val <- continue("This is dangerous"))
expect_true(val)
})
```

If you were testing the behaviour of some function that used `continue()`, you might choose to mock it directly:

```{r}
#| label: mock-continue

save_file <- function(path, data) {
if (file.exists(path)) {
if (!continue("`path` already exists")) {
stop("Failed to continue")
}
}
writeLines(data, path)
}

test_that("save_file() requires confirmation to overwrite file", {
path <- withr::local_tempfile(lines = letters)

local_mocked_bindings(continue = function(...) TRUE)
save_file(path, "a")
expect_equal(readLines(path), "a")

local_mocked_bindings(continue = function(...) FALSE)
expect_snapshot(save_file(path, "a"), error = TRUE)
})
```

## User-facing text

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")`.

## Testing interfaces

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")`.
3 changes: 2 additions & 1 deletion vignettes/custom-expectation.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ vignette: >
%\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
```{r setup}
#| include: false
library(testthat)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")

Expand Down
Loading
Loading