Skip to main content

testing-patterns-with-testthat

Comprehensive guide to testing R packages using testthat 3rd edition, including test structure, expectations, fixtures, and snapshot testing

Source facts

Repository
choxos/RPkgAgent
Last source activity
February 15, 2026 at 21:57
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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 ```r # Setup testthat 3rd edition: usethis::use_testthat(3) ``` This creates: ``` tests/ ├── testthat/ │ └── (test files will go here) └── testthat.R ``` And adds to DESCRIPTION: ```dcf 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 ```r # OLD (Edition 2): context("My feature tests") expect_equal(x, y, tolerance = 1e-8) # NEW (Edition 3): # No context() - file name is context expect_equal(x, y, tolerance = 1e-8) # Same, but better errors ``` ## Test File Structure ### Basic Structure ```r # tests/testthat/test-my-feature.R 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 ```r # tests/testthat/test-statistics.R # Group related tests 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))) }) # Separate feature 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). ```r test_that("numeric equality works", { # Exact equality: expect_equal(1 + 1, 2) # With tolerance (default 1.5e-8): expect_equal(sqrt(2)^2, 2) # Custom tolerance: expect_equal(1.00001, 1, tolerance = 1e-4) # Vectors: expect_equal(1:5, c(1, 2, 3, 4, 5)) # Data frames: expect_equal( data.frame(x = 1:3, y = 4:6), data.frame(x = 1:3, y = 4:6) ) # Ignore attributes: expect_equal( c(a = 1, b = 2), c(1, 2), ignore_attr = TRUE ) }) ``` ### expect_identical() Tests exact identity (no tolerance). ```r test_that("exact identity works", { # Integers: expect_identical(1L, 1L) # But not with doubles: expect_failure(expect_identical(1, 1L)) # Different types # Attributes matter: expect_failure( expect_identical( c(a = 1, b = 2), c(1, 2) ) ) # Use for reference comparisons: x <- 1:10 y <- x expect_identical(x, y) # Type checking: expect_identical(class(x), "integer") }) ``` ### expect_error() Tests that code throws an error. ```r test_that("errors are thrown correctly", { # Basic error: expect_error(stop("oops")) # Error with specific message (regex): expect_error( stop("value must be numeric"), "must be numeric" ) # Error with specific class (PREFERRED): expect_error( my_function(invalid_input), class = "invalid_input_error" ) # Both class and message: expect_error( my_function(NULL), "cannot be NULL", class = "null_input_error" ) }) ``` **Best practice:** Use custom error classes and test them: ```r # In your package: validate_input <- function(x) { if (!is.numeric(x)) { rlang::abort( "Input must be numeric", class = "invalid_input_error" ) } } # In tests: test_that("validation errors have correct class", { expect_error( validate_input("text"), class = "invalid_input_error" ) }) ``` ### expect_warning() Tests that code produces warnings. ```r test_that("warnings are issued correctly", { # Basic warning: expect_warning(warning("careful!")) # Warning with message: expect_warning( my_function(c(1, NA)), "NA values detected" ) # Warning with class: expect_warning( my_function(x), class = "deprecated_argument" ) }) ``` ### expect_message() Tests that code produces messages. ```r test_that("messages are printed correctly", { expect_message(message("Processing...")) expect_message( my_function(verbose = TRUE), "Starting computation" ) # Multiple messages: 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. ```r test_that("clean execution", { # No errors: expect_no_error(my_function(valid_input)) # No warnings: expect_no_warning(my_function(good_data)) # No messages: expect_no_message(my_function(verbose = FALSE)) }) ``` ### Other Useful Expectations ```r test_that("various expectations work", { # Truth values: expect_true(2 + 2 == 4) expect_false(2 + 2 == 5) # NULL: expect_null(NULL) expect_null(my_function_returning_null()) # Type checks: expect_type(1:10, "integer") expect_type(letters, "character") # Class checks: expect_s3_class(lm(y ~ x, data), "lm") expect_s4_class(object, "myS4class") # Length/dimensions: expect_length(1:10, 10) expect_length(list(a = 1, b = 2), 2) # Named: expect_named(c(a = 1, b = 2), c("a", "b")) # Vector type and length: expect_vector(1:10, ptype = integer(), size = 10) # Matching: expect_match("hello world", "hello") expect_match("abc123", "\\d+") # Regex # Set operations: expect_setequal(c(1, 2, 3), c(3, 2, 1)) # Order doesn't matter expect_contains(1:10, c(5, 7, 9)) # Subset # Invisible return: expect_invisible(invisible(42)) # Output: 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. ```r test_that("function output is correct", { expect_snapshot({ my_complex_function() }) }) ``` First run creates `tests/testthat/_snaps/my-test.md`: ```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: ```r testthat::snapshot_review() # Or: testthat::snapshot_accept() ``` ### Snapshot Variants ```r test_that("snapshots capture different outputs", { # Just messages: expect_snapshot( message("Hello"), cnd_class = TRUE # Include condition class ) # Errors (with class): expect_snapshot( error = TRUE, my_function(invalid) ) # Transforming output: expect_snapshot( my_function(), transform = scrub_randomness ) # Multiple variants: expect_snapshot({ cat("Output 1\n") message("Message 1") cat("Output 2\n") }) }) ``` ### expect_snapshot_output() Specifically for printed output (deprecated, use expect_snapshot()). ```r test_that("print methods work", { expect_snapshot_output(print(my_object)) }) ``` ### expect_snapshot_value() Captures R object structure. ```r test_that("complex return values are correct", { result <- my_complex_function() expect_snapshot_value( result, style = "json2" # or "serialize", "deparse" ) }) ``` ### 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: ```r # tests/testthat/helper-data.R # Create test data used across multiple test files: make_test_data <- function(n = 100) { data.frame( id = seq_len(n), value = rnorm(n), category = sample(LETTERS[1:3], n, replace = TRUE) ) } # Create test fixtures: sample_data <- make_test_data() # Utility assertions: 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: ```r # tests/testthat/test-analysis.R test_that("analysis works with test data", { result <- analyze(sample_data) expect_valid_output(result) }) ``` ### Setup Files `setup.R` runs once before all tests: ```r # tests/testthat/setup.R
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub