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

Quellinformationen

Repository
choxos/RPkgAgent
Letzte Quellaktivität
15. Februar 2026 um 21:57
Erkannte Sprache von SKILL.md
Englisch
Sterne
0
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen