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

معلومات المصدر

المستودع
choxos/RPkgAgent
آخر نشاط في المصدر
١٥ فبراير ٢٠٢٦ في ٢١:٥٧
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٠
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub