# Testing R Packages

> Best practices for writing R package tests using testthat version 3+. Use when writing, organizing, or improving tests for R packages. Covers test structure, expectations, fixtures, snapshots, mocking, and modern testthat 3 patterns including self-sufficient tests, proper cleanup with withr, and snapshot testing.

- Skill: `majiayu000/testing-r-packages-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/testing-r-packages-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/testing-r-packages-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/majiayu000/testing-r-packages-2

---


# Testing R Packages with testthat

Modern best practices for R package testing using testthat 3+.

> **Surveyverse projects:** This skill covers generic testthat patterns.
> Project decisions — including the **ban on `describe()` blocks** (flat
> `test_that()` only), coverage targets, `# nocov` policy, and error testing
> patterns — are in `.claude/rules/testing-standards.md`. Package-specific
> conventions (`test_invariants()`, data generators, numerical tolerances) are in
> `.claude/rules/testing-{package}.md`. Those rules take precedence over
> generic guidance here, including any examples in this skill that use `describe()`.

## Initial Setup

Initialize testing with testthat 3rd edition:

```r
usethis::use_testthat(3)
```

This creates `tests/testthat/` directory, adds testthat to `DESCRIPTION` Suggests with `Config/testthat/edition: 3`, and creates `tests/testthat.R`.

## File Organization

**Mirror package structure:**
- Code in `R/foofy.R` → tests in `tests/testthat/test-foofy.R`
- Use `usethis::use_r("foofy")` and `usethis::use_test("foofy")` to create paired files

**Special files:**
- `helper-*.R` - Helper functions and custom expectations, sourced before tests
- `setup-*.R` - Run during `R CMD check` only, not during `load_all()`
- `fixtures/` - Static test data files accessed via `test_path()`

## Test Structure

Tests follow a three-level hierarchy: **File → Test → Expectation**

### Standard Syntax

```r
test_that("descriptive behavior", {
  result <- my_function(input)
  expect_equal(result, expected_value)
})
```

**Test descriptions** should read naturally and describe behavior, not implementation.

### BDD Syntax (describe/it)

For behavior-driven development, use `describe()` and `it()`:

```r
describe("matrix()", {
  it("can be multiplied by a scalar", {
    m1 <- matrix(1:4, 2, 2)
    m2 <- m1 * 2
    expect_equal(matrix(1:4 * 2, 2, 2), m2)
  })

  it("can be transposed", {
    m <- matrix(1:4, 2, 2)
    expect_equal(t(m), matrix(c(1, 3, 2, 4), 2, 2))
  })
})
```

**Key features:**
- `describe()` groups related specifications for a component
- `it()` defines individual specifications (like `test_that()`)
- Supports nesting for hierarchical organization
- `it()` without code creates pending test placeholders

**Use `describe()` to verify you implement the right things, use `test_that()` to ensure you do things right.**

See [references/bdd.md](references/bdd.md) for comprehensive BDD patterns, nested specifications, and test-first workflows.

## Running Tests

Three scales of testing:

**Micro** (interactive development):
```r
devtools::load_all()
expect_equal(foofy(...), expected)
```

**Mezzo** (single file):
```r
testthat::test_file("tests/testthat/test-foofy.R")
# RStudio: Ctrl/Cmd + Shift + T
```

**Macro** (full suite):
```r
devtools::test()    # Ctrl/Cmd + Shift + T
devtools::check()   # Ctrl/Cmd + Shift + E
```

## Core Expectations

### Equality

```r
expect_equal(10, 10 + 1e-7)      # Allows numeric tolerance
expect_identical(10L, 10L)       # Exact match required
expect_all_equal(x, expected)    # Every element matches (v3.3.0+)
```

### Errors, Warnings, Messages

```r
expect_error(1 / "a")
expect_error(bad_call(), class = "specific_error_class")
expect_no_error(valid_call())

expect_warning(deprecated_func())
expect_no_warning(safe_func())

expect_message(informative_func())
expect_no_message(quiet_func())
```

### Pattern Matching

```r
expect_match("Testing is fun!", "Testing")
expect_match(text, "pattern", ignore.case = TRUE)
```

### Structure and Type

```r
expect_length(vector, 10)
expect_type(obj, "list")
expect_s3_class(model, "lm")
expect_s4_class(obj, "MyS4Class")
expect_r6_class(obj, "MyR6Class")      # v3.3.0+
expect_shape(matrix, c(10, 5))         # v3.3.0+
```

### Sets and Collections

```r
expect_setequal(x, y)           # Same elements, any order
expect_contains(fruits, "apple") # Subset check (v3.2.0+)
expect_in("apple", fruits)       # Element in set (v3.2.0+)
expect_disjoint(set1, set2)      # No overlap (v3.3.0+)
```

### Logical

```r
expect_true(condition)
expect_false(condition)
expect_all_true(vector > 0)      # All elements TRUE (v3.3.0+)
expect_all_false(vector < 0)     # All elements FALSE (v3.3.0+)
```

## Design Principles

### 1. Self-Sufficient Tests

Each test should contain all setup, execution, and teardown code:

```r
# Good: self-contained
test_that("foofy() works", {
  data <- data.frame(x = 1:3, y = letters[1:3])
  result <- foofy(data)
  expect_equal(result$x, 1:3)
})

# Bad: relies on ambient state
dat <- data.frame(x = 1:3, y = letters[1:3])
test_that("foofy() works", {
  result <- foofy(dat)  # Where did 'dat' come from?
  expect_equal(result$x, 1:3)
})
```

### 2. Self-Contained Tests (Cleanup Side Effects)

Use `withr` to manage state changes:

```r
test_that("function respects options", {
  withr::local_options(my_option = "test_value")
  withr::local_envvar(MY_VAR = "test")
  withr::local_package("jsonlite")

  result <- my_function()
  expect_equal(result$setting, "test_value")
  # Automatic cleanup after test
})
```

**Common withr functions:**
- `local_options()` - Temporarily set options
- `local_envvar()` - Temporarily set environment variables
- `local_tempfile()` - Create temp file with automatic cleanup
- `local_tempdir()` - Create temp directory with automatic cleanup
- `local_package()` - Temporarily attach package

### 3. Plan for Test Failure

Write tests assuming they will fail and need debugging:
- Tests should run independently in fresh R sessions
- Avoid hidden dependencies on earlier tests
- Make test logic explicit and obvious

### 4. Repetition is Acceptable

Repeat setup code in tests rather than factoring it out. Test clarity is more important than avoiding duplication.

### 5. Use `devtools::load_all()` Workflow

During development:
- Use `devtools::load_all()` instead of `library()`
- Makes all functions available (including unexported)
- Automatically attaches testthat
- Eliminates need for `library()` calls in tests

## Snapshot Testing

For complex output that's difficult to verify programmatically, use snapshot tests. See [references/snapshots.md](references/snapshots.md) for complete guide.

**Basic pattern:**

```r
test_that("error message is helpful", {
  expect_snapshot(
    error = TRUE,
    validate_input(NULL)
  )
})
```

Snapshots stored in `tests/testthat/_snaps/`.

**Workflow:**
```r
devtools::test()                    # Creates new snapshots
testthat::snapshot_review('name')   # Review changes
testthat::snapshot_accept('name')   # Accept changes
```

## Test Fixtures and Data

Three approaches for test data:

**1. Constructor functions** - Create data on-demand:
```r
new_sample_data <- function(n = 10) {
  data.frame(id = seq_len(n), value = rnorm(n))
}
```

**2. Local functions with cleanup** - Handle side effects:
```r
local_temp_csv <- function(data, env = parent.frame()) {
  path <- withr::local_tempfile(fileext = ".csv", .local_envir = env)
  write.csv(data, path, row.names = FALSE)
  path
}
```

**3. Static fixture files** - Store in `fixtures/` directory:
```r
data <- readRDS(test_path("fixtures", "sample_data.rds"))
```

See [references/fixtures.md](references/fixtures.md) for detailed fixture patterns.

## Mocking

Replace external dependencies during testing using `local_mocked_bindings()`. See [references/mocking.md](references/mocking.md) for comprehensive mocking strategies.

**Basic pattern:**

```r
test_that("function works with mocked dependency", {
  local_mocked_bindings(
    external_api = function(...) list(status = "success", data = "mocked")
  )

  result <- my_function_that_calls_api()
  expect_equal(result$status, "success")
})
```

## Common Patterns

See [references/patterns.md](references/patterns.md) for examples: error classes,
temporary files, modified options, multiple cases, custom expectations, file system
discipline, and testthat 3 modernizations.

## Advanced Topics

For advanced testing scenarios, see:

- **[references/bdd.md](references/bdd.md)** - BDD-style testing with describe/it, nested specifications, test-first workflows
- **[references/snapshots.md](references/snapshots.md)** - Snapshot testing, transforms, variants
- **[references/mocking.md](references/mocking.md)** - Mocking strategies, webfakes, httptest2
- **[references/fixtures.md](references/fixtures.md)** - Fixture patterns, database fixtures, helper files
- **[references/patterns.md](references/patterns.md)** - Common patterns, file system discipline, testthat 3 modernizations
- **[references/advanced.md](references/advanced.md)** - Skipping tests, secrets management, CRAN requirements, custom expectations, parallel testing

## Quick Reference

**Initialize:** `usethis::use_testthat(3)`

**Run tests:** `devtools::test()` or Ctrl/Cmd + Shift + T

**Create test file:** `usethis::use_test("name")`

**Review snapshots:** `testthat::snapshot_review()`

**Accept snapshots:** `testthat::snapshot_accept()`

**Find slow tests:** `devtools::test(reporter = "slow")`

**Shuffle tests:** `devtools::test(shuffle = TRUE)`

