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.
npx skills add https://github.com/posit-dev/skills --skill testing-r-packages
Modern best practices for R package testing using testthat 3+.
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.
Mirror package structure:
R/foofy.R → tests in tests/testthat/test-foofy.Rusethis::use_r("foofy") and usethis::use_test("foofy") to create paired filesSpecial files:
helper-*.R - Helper functions and custom expectations, sourced before testssetup-*.R - Run during R CMD check only, not during load_all()fixtures/ - Static test data files accessed via test_path()Tests follow a three-level hierarchy: File → Test → Expectation
test_that("descriptive behavior", {
result <- my_function(input)
expect_equal(result, expected_value)
})
Test descriptions should read naturally and describe behavior, not implementation.
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:
describe() groups related specifications for a componentit() defines individual specifications (like test_that())it() without code creates pending test placeholdersUse describe() to verify you implement the right things, use test_that() to ensure you do things right.
See references/bdd.md for comprehensive BDD patterns, nested specifications, and test-first workflows.
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
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+)
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())
expect_match("Testing is fun!", "Testing")
expect_match(text, "pattern", ignore.case = TRUE)
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+
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+)
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+)
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)
})
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:
local_options() - Temporarily set optionslocal_envvar() - Temporarily set environment variableslocal_tempfile() - Create temp file with automatic cleanuplocal_tempdir() - Create temp directory with automatic cleanuplocal_package() - Temporarily attach packageWrite tests assuming they will fail and need debugging:
Repeat setup code in tests rather than factoring it out. Test clarity is more important than avoiding duplication.
devtools::load_all() WorkflowDuring development:
devtools::load_all() instead of library()library() calls in testsFor 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
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.
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")
})
test_that("validation catches errors", {
expect_error(
validate_input("wrong_type"),
class = "vctrs_error_cast"
)
})
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)
})
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)
})
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")
})
# 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("[email protected]")
expect_valid_user(user)
})
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")
For advanced testing scenarios, see:
When working with testthat 3 code, prefer modern patterns:
Deprecated → Modern:
context() → Remove (duplicates filename)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:
Config/testthat/edition: 3)waldo::compare() for better diff outputlocal_mocked_bindings() works with byte-compiled codeInitialize: 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)
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when implementing any feature or bugfix, before writing implementation code
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
Expert guidance for systematic backtesting of trading strategies. Use when developing, testing, stress-testing, or validating quantitative trading strategies. Covers "beating ideas to death" methodology, parameter robustness testing, slippage modeling, bias prevention, and interpreting backtest results. Applicable when user asks about backtesting, strategy validation, robustness testing, avoiding overfitting, or systematic trading development.
Cloud laboratory platform for automated protein testing and validation. Use when designing proteins and needing experimental validation including binding assays, expression testing, thermostability measurements, enzyme activity assays, or protein sequence optimization. Also use for submitting experiments via API, tracking experiment status, downloading results, optimizing protein sequences for better expression using computational tools (NetSolP, SoluProt, SolubleMPNN, ESM), or managing protein design workflows with wet-lab validation.
This skill should be used for time series machine learning tasks including classification, regression, clustering, forecasting, anomaly detection, segmentation, and similarity search. Use when working with temporal data, sequential patterns, or time-indexed observations requiring specialized algorithms beyond standard ML approaches. Particularly suited for univariate and multivariate time series analysis with scikit-learn compatible APIs.
Take posit-dev/testing-r-packages from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.