Skills testing-r-packages
git clone https://github.com/posit-dev/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/posit-dev/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/r-lib/testing-r-packages" ~/.claude/skills/posit-dev-skills-testing-r-packages && rm -rf "$T"
r-lib/testing-r-packages/SKILL.mdTesting R Packages with testthat
Modern best practices for R package testing using testthat 3+.
Initial Setup
Initialize testing with testthat 3rd edition:
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
→ tests inR/foofy.Rtests/testthat/test-foofy.R - Use
andusethis::use_r("foofy")
to create paired filesusethis::use_test("foofy")
Special files:
- Helper functions and custom expectations, sourced before testshelper-*.R
- Run duringsetup-*.R
only, not duringR CMD checkload_all()
- Static test data files accessed viafixtures/test_path()
Test Structure
Tests follow a three-level hierarchy: File → Test → Expectation
Standard Syntax
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():
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:
groups related specifications for a componentdescribe()
defines individual specifications (likeit()
)test_that()- Supports nesting for hierarchical organization
without code creates pending test placeholdersit()
Use
to verify you implement the right things, use describe()
to ensure you do things right.test_that()
See references/bdd.md for comprehensive BDD patterns, nested specifications, and test-first workflows.
Running Tests
Three scales of testing:
Micro (interactive development):
devtools::load_all() expect_equal(foofy(...), expected)
Mezzo (single file):
testthat::test_file("tests/testthat/test-foofy.R") # RStudio: Ctrl/Cmd + Shift + T
Macro (full suite):
devtools::test() # Ctrl/Cmd + Shift + T devtools::check() # Ctrl/Cmd + Shift + E
Core Expectations
Equality
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
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
expect_match("Testing is fun!", "Testing") expect_match(text, "pattern", ignore.case = TRUE)
Structure and Type
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
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
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:
# 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:
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:
- Temporarily set optionslocal_options()
- Temporarily set environment variableslocal_envvar()
- Create temp file with automatic cleanuplocal_tempfile()
- Create temp directory with automatic cleanuplocal_tempdir()
- Temporarily attach packagelocal_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
devtools::load_all()During development:
- Use
instead ofdevtools::load_all()library() - Makes all functions available (including unexported)
- Automatically attaches testthat
- Eliminates need for
calls in testslibrary()
Snapshot Testing
For complex output that's difficult to verify programmatically, use snapshot tests. See references/snapshots.md for complete guide.
Basic pattern:
test_that("error message is helpful", { expect_snapshot( error = TRUE, validate_input(NULL) ) })
Snapshots stored in
tests/testthat/_snaps/.
Workflow:
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:
new_sample_data <- function(n = 10) { data.frame(id = seq_len(n), value = rnorm(n)) }
2. Local functions with cleanup - Handle side effects:
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:
data <- readRDS(test_path("fixtures", "sample_data.rds"))
See references/fixtures.md for detailed fixture patterns.
Mocking
Replace external dependencies during testing using
local_mocked_bindings(). See references/mocking.md for comprehensive mocking strategies.
Basic pattern:
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
Testing Errors with Specific Classes
test_that("validation catches errors", { expect_error( validate_input("wrong_type"), class = "vctrs_error_cast" ) })
Testing with Temporary Files
test_that("file processing works", { temp_file <- withr::local_tempfile( lines = c("line1", "line2", "line3") ) result <- process_file(temp_file) expect_equal(length(result), 3) })
Testing with Modified Options
test_that("output respects width", { withr::local_options(width = 40) output <- capture_output(print(my_object)) expect_lte(max(nchar(strsplit(output, "\n")[[1]])), 40) })
Testing Multiple Related Cases
test_that("str_trunc() handles all directions", { trunc <- function(direction) { str_trunc("This string is moderately long", direction, width = 20) } expect_equal(trunc("right"), "This string is mo...") expect_equal(trunc("left"), "...erately long") expect_equal(trunc("center"), "This stri...ely long") })
Custom Expectations in Helper Files
# In tests/testthat/helper-expectations.R expect_valid_user <- function(user) { expect_type(user, "list") expect_named(user, c("id", "name", "email")) expect_type(user$id, "integer") expect_match(user$email, "@") } # In test file test_that("user creation works", { user <- create_user("test@example.com") expect_valid_user(user) })
File System Discipline
Always write to temp directory:
# Good output <- withr::local_tempfile(fileext = ".csv") write.csv(data, output) # Bad - writes to package directory write.csv(data, "output.csv")
Access test fixtures with
:test_path()
# Good - works in all contexts data <- readRDS(test_path("fixtures", "data.rds")) # Bad - relative paths break data <- readRDS("fixtures/data.rds")
Advanced Topics
For advanced testing scenarios, see:
- references/bdd.md - BDD-style testing with describe/it, nested specifications, test-first workflows
- references/snapshots.md - Snapshot testing, transforms, variants
- references/mocking.md - Mocking strategies, webfakes, httptest2
- references/fixtures.md - Fixture patterns, database fixtures, helper files
- references/advanced.md - Skipping tests, secrets management, CRAN requirements, custom expectations, parallel testing
testthat 3 Modernizations
When working with testthat 3 code, prefer modern patterns:
Deprecated → Modern:
→ Remove (duplicates filename)context()
→expect_equivalent()expect_equal(ignore_attr = TRUE)
→with_mock()local_mocked_bindings()
,is_null()
,is_true()
→is_false()
,expect_null()
,expect_true()expect_false()
New in testthat 3:
- Edition system (
)Config/testthat/edition: 3 - Improved snapshot testing
for better diff outputwaldo::compare()- Unified condition handling
works with byte-compiled codelocal_mocked_bindings()- Parallel test execution support
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)