Testing 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
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
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:
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 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")
Macro (full suite):
devtools::test()
devtools::check()
Core Expectations
Equality
expect_equal(10, 10 + 1e-7)
expect_identical(10L, 10L)
expect_all_equal(x, expected)
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")
expect_shape(matrix, c(10, 5))
Sets and Collections
expect_setequal(x, y)
expect_contains(fruits, "apple")
expect_in("apple", fruits)
expect_disjoint(set1, set2)
Logical
expect_true(condition)
expect_false(condition)
expect_all_true(vector > 0)
expect_all_false(vector < 0)
Design Principles
1. Self-Sufficient Tests
Each test should contain all setup, execution, and teardown code:
test_that("foofy() works", {
data <- data.frame(x = 1:3, y = letters[1:3])
result <- foofy(data)
expect_equal(result$x, 1:3)
})
dat <- data.frame(x = 1:3, y = letters[1:3])
test_that("foofy() works", {
result <- foofy(dat)
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")
})
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 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()
testthat::snapshot_review('name')