| name | Testing Patterns with testthat |
| description | Comprehensive guide to testing R packages using testthat 3rd edition, including test structure, expectations, fixtures, and snapshot testing |
Testing Patterns with testthat
Overview
Testing is essential for reliable R packages. This skill covers testthat 3rd edition, the standard testing framework for R packages, including test structure, expectations, fixtures, and advanced patterns.
Setup
Initial Setup
usethis::use_testthat(3)
This creates:
tests/
├── testthat/
│ └── (test files will go here)
└── testthat.R
And adds to DESCRIPTION:
Suggests:
testthat (>= 3.0.0)
Config/testthat/edition: 3
Key Differences: Edition 3 vs 2
Edition 3 changes:
context() deprecated (use file names)
- More informative error messages
- Better snapshot testing
- Improved parallel test support
- Stricter comparison defaults
context("My feature tests")
expect_equal(x, y, tolerance = 1e-8)
expect_equal(x, y, tolerance = 1e-8)
Test File Structure
Basic Structure
test_that("basic functionality works", {
result <- my_function(1:10)
expect_equal(result, (1:10) * 2)
expect_length(result, 10)
})
test_that("handles edge cases", {
expect_equal(my_function(numeric(0)), numeric(0))
expect_error(my_function(NULL), class = "error")
})
test_that("parameter validation works", {
expect_error(my_function("not numeric"), "must be numeric")
expect_warning(my_function(c(1, NA)), "NA values detected")
})
File Naming Convention
Mirror your R/ file structure:
R/
├── data-processing.R
├── visualization.R
└── utils.R
tests/testthat/
├── test-data-processing.R
├── test-visualization.R
└── test-utils.R
Rules:
- Files MUST start with
test-
- Use descriptive names matching R/ files
- One test file per R source file (usually)
- Or organize by feature/functionality
Test Organization Within Files
test_that("mean calculation is correct", {
expect_equal(my_mean(1:10), 5.5)
expect_equal(my_mean(c(1, 2, 3)), 2)
})
test_that("mean handles NA values", {
expect_equal(my_mean(c(1, NA, 3), na.rm = TRUE), 2)
expect_true(is.na(my_mean(c(1, NA, 3), na.rm = FALSE)))
})
test_that("mean validates input", {
expect_error(my_mean("not numeric"))
expect_error(my_mean(list(1, 2, 3)))
})
test_that("median calculation is correct", {
expect_equal(my_median(1:10), 5.5)
expect_equal(my_median(1:11), 6)
})
Core Expectations
expect_equal()
Tests near equality (with tolerance for numerics).
test_that("numeric equality works", {
expect_equal(1 + 1, 2)
expect_equal(sqrt(2)^2, 2)
expect_equal(1.00001, 1, tolerance = 1e-4)
expect_equal(1:5, c(1, 2, 3, 4, 5))
expect_equal(
data.frame(x = 1:3, y = 4:6),
data.frame(x = 1:3, y = 4:6)
)
expect_equal(
c(a = 1, b = 2),
c(1, 2),
ignore_attr = TRUE
)
})
expect_identical()
Tests exact identity (no tolerance).
test_that("exact identity works", {
expect_identical(1L, 1L)
expect_failure(expect_identical(1, 1L))
expect_failure(
expect_identical(
c(a = 1, b = 2),
c(1, 2)
)
)
x <- 1:10
y <- x
expect_identical(x, y)
expect_identical(class(x), "integer")
})
expect_error()
Tests that code throws an error.
test_that("errors are thrown correctly", {
expect_error(stop("oops"))
expect_error(
stop("value must be numeric"),
"must be numeric"
)
expect_error(
my_function(invalid_input),
class = "invalid_input_error"
)
expect_error(
my_function(NULL),
"cannot be NULL",
class = "null_input_error"
)
})
Best practice: Use custom error classes and test them:
validate_input <- function(x) {
if (!is.numeric(x)) {
rlang::abort(
"Input must be numeric",
class = "invalid_input_error"
)
}
}
test_that("validation errors have correct class", {
expect_error(
validate_input("text"),
class = "invalid_input_error"
)
})
expect_warning()
Tests that code produces warnings.
test_that("warnings are issued correctly", {
expect_warning(warning("careful!"))
expect_warning(
my_function(c(1, NA)),
"NA values detected"
)
expect_warning(
my_function(x),
class = "deprecated_argument"
)
})
expect_message()
Tests that code produces messages.
test_that("messages are printed correctly", {
expect_message(message("Processing..."))
expect_message(
my_function(verbose = TRUE),
"Starting computation"
)
expect_message(
expect_message(
my_verbose_function(),
"Step 1"
),
"Step 2"
)
})
expect_no_error() / expect_no_warning() / expect_no_message()
Tests that code runs without conditions.
test_that("clean execution", {
expect_no_error(my_function(valid_input))
expect_no_warning(my_function(good_data))
expect_no_message(my_function(verbose = FALSE))
})
Other Useful Expectations
test_that("various expectations work", {
expect_true(2 + 2 == 4)
expect_false(2 + 2 == 5)
expect_null(NULL)
expect_null(my_function_returning_null())
expect_type(1:10, "integer")
expect_type(letters, "character")
expect_s3_class(lm(y ~ x, data), "lm")
expect_s4_class(object, "myS4class")
expect_length(1:10, 10)
expect_length(list(a = 1, b = 2), 2)
expect_named(c(a = 1, b = 2), c("a", "b"))
expect_vector(1:10, ptype = integer(), size = 10)
expect_match("hello world", "hello")
expect_match("abc123", "\\d+")
expect_setequal(c(1, 2, 3), c(3, 2, 1))
expect_contains(1:10, c(5, 7, 9))
expect_invisible(invisible(42))
expect_output(print("hello"), "hello")
})
Snapshot Testing
Test output that's hard to describe with expectations.
expect_snapshot()
Captures printed output, messages, warnings, and errors.
test_that("function output is correct", {
expect_snapshot({
my_complex_function()
})
})
First run creates tests/testthat/_snaps/my-test.md:
# function output is correct
Code
my_complex_function()
Output
Processing data...
Results:
Mean: 5.5
SD: 2.87
Subsequent runs compare against snapshot. Update with:
testthat::snapshot_review()
testthat::snapshot_accept()
Snapshot Variants
test_that("snapshots capture different outputs", {
expect_snapshot(
message("Hello"),
cnd_class = TRUE
)
expect_snapshot(
error = TRUE,
my_function(invalid)
)
expect_snapshot(
my_function(),
transform = scrub_randomness
)
expect_snapshot({
cat("Output 1\n")
message("Message 1")
cat("Output 2\n")
})
})
expect_snapshot_output()
Specifically for printed output (deprecated, use expect_snapshot()).
test_that("print methods work", {
expect_snapshot_output(print(my_object))
})
expect_snapshot_value()
Captures R object structure.
test_that("complex return values are correct", {
result <- my_complex_function()
expect_snapshot_value(
result,
style = "json2"
)
})
When to Use Snapshots
Good for:
- Complex printed output (print methods, summaries)
- Error messages (ensures consistent UX)
- Multi-line formatted output
- Plots (as text representation)
Avoid for:
- Simple values (use expect_equal())
- When snapshots are hard to review
- When output changes frequently
Test Helpers and Fixtures
Helper Files
Files starting with helper- run before tests and make utilities available:
make_test_data <- function(n = 100) {
data.frame(
id = seq_len(n),
value = rnorm(n),
category = sample(LETTERS[1:3], n, replace = TRUE)
)
}
sample_data <- make_test_data()
expect_valid_output <- function(x) {
expect_s3_class(x, "data.frame")
expect_true(nrow(x) > 0)
expect_named(x, c("id", "result"))
}
Use in tests:
test_that("analysis works with test data", {
result <- analyze(sample_data)
expect_valid_output(result)
})
Setup Files
setup.R runs once before all tests: