testing-r-packages
Original:🇺🇸 English
Translated
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.
4installs
Sourceposit-dev/skills
Added on
NPX Install
npx skill4agent add posit-dev/skills testing-r-packagesTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Testing R Packages with testthat
Modern best practices for R package testing using testthat 3+.
Initial Setup
Initialize testing with testthat 3rd edition:
r
usethis::use_testthat(3)This creates directory, adds testthat to Suggests with , and creates .
tests/testthat/DESCRIPTIONConfig/testthat/edition: 3tests/testthat.RFile Organization
Mirror package structure:
- Code in → tests in
R/foofy.Rtests/testthat/test-foofy.R - Use and
usethis::use_r("foofy")to create paired filesusethis::use_test("foofy")
Special files:
- - Helper functions and custom expectations, sourced before tests
helper-*.R - - Run during
setup-*.Ronly, not duringR CMD checkload_all() - - Static test data files accessed via
fixtures/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 and :
describe()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:
- groups related specifications for a component
describe() - defines individual specifications (like
it())test_that() - Supports nesting for hierarchical organization
- without code creates pending test placeholders
it()
Use to verify you implement the right things, use to ensure you do things right.
describe()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):
r
devtools::load_all()
expect_equal(foofy(...), expected)Mezzo (single file):
r
testthat::test_file("tests/testthat/test-foofy.R")
# RStudio: Ctrl/Cmd + Shift + TMacro (full suite):
r
devtools::test() # Ctrl/Cmd + Shift + T
devtools::check() # Ctrl/Cmd + Shift + ECore 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 to manage state changes:
withrr
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 options
local_options() - - Temporarily set environment variables
local_envvar() - - Create temp file with automatic cleanup
local_tempfile() - - Create temp directory with automatic cleanup
local_tempdir() - - Temporarily attach package
local_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 of
devtools::load_all()library() - Makes all functions available (including unexported)
- Automatically attaches testthat
- Eliminates need for calls in tests
library()
Snapshot Testing
For complex output that's difficult to verify programmatically, use snapshot tests. See 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 changesTest 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 directory:
fixtures/r
data <- readRDS(test_path("fixtures", "sample_data.rds"))See references/fixtures.md for detailed fixture patterns.
Mocking
Replace external dependencies during testing using . See references/mocking.md for comprehensive mocking strategies.
local_mocked_bindings()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
Testing Errors with Specific Classes
r
test_that("validation catches errors", {
expect_error(
validate_input("wrong_type"),
class = "vctrs_error_cast"
)
})Testing with Temporary Files
r
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
r
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
r
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
r
# 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:
r
# 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()r
# 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 output
waldo::compare() - Unified condition handling
- works with byte-compiled code
local_mocked_bindings() - Parallel test execution support
Quick Reference
Initialize:
usethis::use_testthat(3)Run tests: or Ctrl/Cmd + Shift + T
devtools::test()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)