R package testing with testthat 3rd edition. Use when writing R tests, fixing failing tests, debugging errors, or reviewing coverage—e.g., "write testthat tests", "fix failing R tests", "snapshot testing", "test coverage".
90
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
Provide modern best practices for R package testing using testthat 3+. Guide test creation, fixture design, snapshot testing, and BDD-style specifications. This skill enforces testing discipline—critical practices are non-negotiable.
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.
YOU MUST 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:
setup-*.R - Run during R CMD check only, not during load_all()fixtures/ - Static test data files accessed via test_path()helper-*.R - Helper functions and custom expectations, sourced before tests
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.
it is highly encouraged to write descriptions using glue::glue() for dynamic content:
DESCRIPTION Suggests if you use it in your tests.test_that(glue::glue("{fixture_name} returns {expected_value} for input {input}"), {
result <- my_function(input)
expect_equal(result, expected_value)
})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")Macro (full suite):
devtools::test()
devtools::check()YOU MUST use withr to manage state changes. Tests without withr::local_* = leaked state. Every time.
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 packageYOU MUST write 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()—NEVER use library() for package under testlibrary() 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—YOU MUST complete all steps:
devtools::test() # Creates new snapshots
# IMMEDIATELY after creating snapshots:
testthat::snapshot_review('name') # Review changes—never skip this stepUnreviewed snapshots = undetected regressions. Every time.
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.
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("test@example.com")
expect_valid_user(user)
})YOU MUST ALWAYS write to temp directory—no exceptions:
# Good
output <- withr::local_tempfile(fileext = ".csv")
write.csv(data, output)
# Bad - writes to package directory
write.csv(data, "output.csv")ALWAYS access test fixtures with test_path()—relative paths break in CI:
# Good—ALWAYS use test_path()
data <- readRDS(test_path("fixtures", "data.rds"))
# Bad—relative paths cause CI failures. Every time.
data <- readRDS("fixtures/data.rds")a8e2aab
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.