testing-r-packages
Best practices for writing R package tests using testthat version 3+. Use when writing, organizing, or improving tests for R packages. Covers test structure, expectations, fixtures, snapshots, mocking, and modern testthat 3 patterns including self-sufficient tests, proper cleanup
Install
npx skills add https://github.com/posit-dev/skills/tree/main/r-lib/testing-r-packages
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
git clone https://github.com/posit-dev/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole posit-dev/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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 intests/testthat/test-foofy.R - Use
usethis::use_r("foofy")andusethis::use_test("foofy")to create paired files
Special files:
helper-*.R- Helper functions and custom expectations, sourced before testssetup-*.R- Run duringR CMD checkonly, not duringload_all()fixtures/- Static test data files accessed viatest_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 componentit()defines individual specifications (liketest_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")
# RStudio: Ctrl/Cmd + Shift + T
Macro (full suite):
devtools::test() # Ctrl/Cmd + Shift + T
devtools::check() # Ctrl/Cmd + Shift + E
Core Expectations
Equality
expect_equal(10, 10 + 1e-7) # Allows numeric tolerance
expect_identical(10L, 10L) # Exact match required
expect_all_equal(x, expected) # Every element matches (v3.3.0+)
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") # v3.3.0+
expect_shape(matrix, c(10, 5)) # v3.3.0+
Sets and Collections
expect_setequal(x, y) # Same elements, any order
expect_contains(fruits, "apple") # Subset check (v3.2.0+)
expect_in("apple", fruits) # Element in set (v3.2.0+)
expect_disjoint(set1, set2) # No overlap (v3.3.0+)
Logical
expect_true(condition)
expect_false(condition)
expect_all_true(vector > 0) # All elements TRUE (v3.3.0+)
expect_all_false(vector < 0) # All elements FALSE (v3.3.0+)
Design Principles
1. Self-Sufficient Tests
Each test should contain all setup, execution, and teardown code:
# Good: self-contained
test_that("foofy() works", {
data <- data.frame(x = 1:3, y = letters[1:3])
result <- foofy(data)
expect_equal(result$x, 1:3)
})
# Bad: relies on ambient state
dat <- data.frame(x = 1:3, y = letters[1:3])
test_that("foofy() works", {
result <- foofy(dat) # Where did 'dat' come from?
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")
# Automatic cleanup after test
})
Common withr functions:
local_options()- Temporarily set optionslocal_envvar()- Temporarily set environment variableslocal_tempfile()- Create temp file with automatic cleanuplocal_tempdir()- Create temp directory with automatic cleanuplocal_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 oflibrary() - 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() # Creates new snapshots
testthat::snapshot_review('name') # Review changes
testthat::snapshot_accept('name') # Accept changes
Test Fixtures and Data
Three approaches for test data:
1. Constructor functions - Create data on-demand:
new_sample_data <- function(n = 10) {
data.frame(id = seq_len(n), value = rnorm(n))
}
2. Local functions with cleanup - Handle side effects:
local_temp_csv <- function(data, env = parent.frame()) {
path <- withr::local_tempfile(fileext = ".csv", .local_envir = env)
write.csv(data, path, row.names = FALSE)
path
}
3. Static fixture files - Store in fixtures/ directory:
data <- readRDS(test_path("fixtures", "sample_data.rds"))
See references/fixtures.md for detailed fixture patterns.
Mocking
Replace external dependencies during testing using local_mocked_bindings(). See references/mocking.md for comprehensive mocking strategies.
Basic pattern:
test_that("function works with mocked dependency", {
local_mocked_bindings(
external_api = function(...) list(status = "success", data = "mocked")
)
result <- my_function_that_calls_api()
expect_equal(result$status, "success")
})
Common Patterns
Testing Errors with Specific Classes
test_that("validation catches errors", {
expect_error(
validate_input("wrong_type"),
class = "vctrs_error_cast"
)
})
Testing with Temporary Files
test_that("file processing works", {
temp_file <- withr::local_tempfile(
lines = c("line1", "line2", "line3")
)
result <- process_file(temp_file)
expect_equal(length(result), 3)
})
Testing with Modified Options
test_that("output respects width", {
withr::local_options(width = 40)
output <- capture_output(print(my_object))
expect_lte(max(nchar(strsplit(output, "\n")[[1]])), 40)
})
Testing Multiple Related Cases
test_that("str_trunc() handles all directions", {
trunc <- function(direction) {
str_trunc("This string is moderately long", direction, width = 20)
}
expect_equal(trunc("right"), "This string is mo...")
expect_equal(trunc("left"), "...erately long")
expect_equal(trunc("center"), "This stri...ely long")
})
Custom Expectations in Helper Files
# In tests/testthat/helper-expectations.R
expect_valid_user <- function(user) {
expect_type(user, "list")
expect_named(user, c("id", "name", "email"))
expect_type(user$id, "integer")
expect_match(user$email, "@")
}
# In test file
test_that("user creation works", {
user <- create_user("test@example.com")
expect_valid_user(user)
})
File System Discipline
Always write to temp directory:
# Good
output <- withr::local_tempfile(fileext = ".csv")
write.csv(data, output)
# Bad - writes to package directory
write.csv(data, "output.csv")
Access test fixtures with test_path():
# Good - works in all contexts
data <- readRDS(test_path("fixtures", "data.rds"))
# Bad - relative paths break
data <- readRDS("fixtures/data.rds")
Advanced Topics
For advanced testing scenarios, see:
- references/bdd.md - BDD-style testing with describe/it, nested specifications, test-first workflows
- references/snapshots.md - Snapshot testing, transforms, variants
- references/mocking.md - Mocking strategies, webfakes, httptest2
- references/fixtures.md - Fixture patterns, database fixtures, helper files
- references/advanced.md - Skipping tests, secrets management, CRAN requirements, custom expectations, parallel testing
testthat 3 Modernizations
When working with testthat 3 code, prefer modern patterns:
Deprecated → Modern:
context()→ Remove (duplicates filename)expect_equivalent()→expect_equal(ignore_attr = TRUE)with_mock()→local_mocked_bindings()is_null(),is_true(),is_false()→expect_null(),expect_true(),expect_false()
New in testthat 3:
- Edition system (
Config/testthat/edition: 3) - Improved snapshot testing
waldo::compare()for better diff output- Unified condition handling
local_mocked_bindings()works with byte-compiled code- Parallel test execution support
Quick Reference
Initialize: usethis::use_testthat(3)
Run tests: devtools::test() or Ctrl/Cmd + Shift + T
Create test file: usethis::use_test("name")
Review snapshots: testthat::snapshot_review()
Accept snapshots: testthat::snapshot_accept()
Find slow tests: devtools::test(reporter = "slow")
Shuffle tests: devtools::test(shuffle = TRUE)
Files (skills)
-
references
-
advanced.md 7.8 KB
# Advanced Testing Topics ## Skipping Tests Skip tests conditionally when requirements aren't met: ### Built-in Skip Functions ```r test_that("API integration works", { skip_if_offline() skip_if_not_installed("httr2") skip_on_cran() skip_on_os("windows") result <- call_external_api() expect_true(result$success) }) ``` **Common skip functions:** - `skip()` - Skip unconditionally with message - `skip_if()` - Skip if condition is TRUE - `skip_if_not()` - Skip if condition is FALSE - `skip_if_offline()` - Skip if no internet - `skip_if_not_installed(pkg)` - Skip if package unavailable - `skip_on_cran()` - Skip on CRAN checks - `skip_on_os(os)` - Skip on specific OS - `skip_on_ci()` - Skip on continuous integration - `skip_unless_r(version)` - Skip unless R version requirement met (testthat 3.3.0+) ### Custom Skip Conditions ```r skip_if_no_api_key <- function() { if (Sys.getenv("API_KEY") == "") { skip("API_KEY not available") } } skip_if_slow <- function() { if (!identical(Sys.getenv("RUN_SLOW_TESTS"), "true")) { skip("Slow tests not enabled") } } test_that("authenticated endpoint works", { skip_if_no_api_key() result <- call_authenticated_endpoint() expect_equal(result$status, "success") }) ``` ## Testing Flaky Code ### retry with `try_again()` Test code that may fail occasionally (network calls, timing-dependent code): ```r test_that("flaky network call succeeds eventually", { result <- try_again( times = 3, { response <- make_network_request() expect_equal(response$status, 200) response } ) expect_type(result, "list") }) ``` ### Mark Tests as Flaky ```r test_that("timing-sensitive operation", { skip_on_cran() # Too unreliable for CRAN start <- Sys.time() result <- async_operation() duration <- as.numeric(Sys.time() - start) expect_lt(duration, 2) # Should complete in < 2 seconds }) ``` ## Managing Secrets in Tests ### Environment Variables ```r test_that("authenticated API works", { # Skip if credentials unavailable api_key <- Sys.getenv("MY_API_KEY") skip_if(api_key == "", "MY_API_KEY not set") result <- call_api(api_key) expect_true(result$authenticated) }) ``` ### Local Configuration Files ```r test_that("service integration works", { config_path <- test_path("fixtures", "test_config.yml") skip_if_not(file.exists(config_path), "Test config not found") config <- yaml::read_yaml(config_path) result <- connect_to_service(config) expect_true(result$connected) }) ``` **Never commit secrets:** - Add config files with secrets to `.gitignore` - Use environment variables in CI/CD - Provide example config files: `test_config.yml.example` ### Testing Without Secrets Design tests to degrade gracefully: ```r test_that("API client works", { api_key <- Sys.getenv("API_KEY") if (api_key == "") { # Mock the API when credentials unavailable local_mocked_bindings( make_api_call = function(...) list(status = "success", data = "mocked") ) } result <- my_api_wrapper() expect_equal(result$status, "success") }) ``` ## Custom Expectations Create domain-specific expectations for clearer tests: ### Simple Custom Expectations ```r # In helper-expectations.R expect_valid_email <- function(email) { expect_match(email, "^[^@]+@[^@]+\\.[^@]+$") } expect_positive <- function(x) { expect_true(all(x > 0), info = "All values should be positive") } expect_named_list <- function(object, names) { expect_type(object, "list") expect_named(object, names, ignore.order = TRUE) } ``` Usage: ```r test_that("user validation works", { user <- create_user("test@example.com") expect_valid_email(user$email) }) ``` ### Complex Custom Expectations ```r expect_valid_model <- function(model) { act <- quasi_label(rlang::enquo(model)) expect( inherits(act$val, "lm") && !is.null(act$val$coefficients), sprintf("%s is not a valid linear model", act$lab) ) invisible(act$val) } ``` ## State Inspection Detect unintended global state changes: ```r # In setup-state.R set_state_inspector(function() { list( options = options(), env_vars = Sys.getenv(), search = search() ) }) ``` testthat will warn if state changes between tests. ## CRAN-Specific Considerations ### Time Limits Tests must complete in under 1 minute: ```r test_that("slow operation completes", { skip_on_cran() # Takes 2 minutes result <- expensive_computation() expect_equal(result$status, "complete") }) ``` ### File System Discipline Only write to temp directory: ```r test_that("file output works", { # Good output <- withr::local_tempfile(fileext = ".csv") write.csv(data, output) # Bad - writes to package directory # write.csv(data, "output.csv") }) ``` ### No External Dependencies Avoid relying on: - Network access - External processes - System commands - Clipboard access ```r test_that("external dependency", { skip_on_cran() # Code requiring network or system calls }) ``` ### Platform Differences Use `expect_equal()` for numeric comparisons (allows tolerance): ```r test_that("calculation works", { result <- complex_calculation() # Good: tolerant to floating point differences expect_equal(result, 1.234567) # Bad: fails due to platform differences # expect_identical(result, 1.234567) }) ``` ## Test Performance ### Identify Slow Tests ```r devtools::test(reporter = "slow") ``` The `SlowReporter` highlights performance bottlenecks. ### Test Shuffling Detect unintended test dependencies: ```r # Randomly reorder tests devtools::test(shuffle = TRUE) # In test file test_dir("tests/testthat", shuffle = TRUE) ``` If tests fail when shuffled, they have unintended dependencies on execution order. ## Parallel Testing Enable parallel test execution in `DESCRIPTION`: ``` Config/testthat/parallel: true ``` **Requirements for parallel tests:** - Tests must be independent - No shared state between tests - Use `local_*()` functions for all side effects - Snapshot tests work correctly in parallel (testthat 3.2.0+) ## Testing Edge Cases ### Boundary Conditions ```r test_that("handles boundary conditions", { expect_equal(my_func(0), expected_at_zero) expect_equal(my_func(-1), expected_negative) expect_equal(my_func(Inf), expected_infinite) expect_true(is.nan(my_func(NaN))) }) ``` ### Empty Inputs ```r test_that("handles empty inputs", { expect_equal(process(character()), character()) expect_equal(process(NULL), NULL) expect_equal(process(data.frame()), data.frame()) }) ``` ### Type Validation ```r test_that("validates input types", { expect_error(my_func("string"), class = "vctrs_error_cast") expect_error(my_func(list()), "must be atomic") expect_no_error(my_func(1:10)) }) ``` ## Debugging Failed Tests ### Interactive Debugging ```r # Run test interactively devtools::load_all() test_that("problematic test", { # Add browser() to pause execution browser() result <- problematic_function() expect_equal(result, expected) }) ``` ### Print Debugging in Tests ```r test_that("debug output", { data <- prepare_data() print(str(data)) # Visible when test fails result <- process(data) print(result) expect_equal(result, expected) }) ``` ### Capture Output for Inspection ```r test_that("inspect messages", { messages <- capture_messages( result <- function_with_messages() ) print(messages) # See all messages when test fails expect_match(messages, "Processing complete") }) ``` ## Testing R6 Classes ```r test_that("R6 class works", { obj <- MyClass$new(value = 10) expect_r6_class(obj, "MyClass") # testthat 3.3.0+ expect_equal(obj$value, 10) obj$increment() expect_equal(obj$value, 11) }) ``` ## Testing S4 Classes ```r test_that("S4 validity works", { obj <- new("MyClass", slot1 = 10, slot2 = "test") expect_s4_class(obj, "MyClass") expect_equal(obj@slot1, 10) expect_error( new("MyClass", slot1 = -1), "slot1 must be positive" ) }) ``` -
bdd.md 10.3 KB
# BDD-Style Testing with describe() and it() Behavior-Driven Development (BDD) testing uses `describe()` and `it()` to create specification-style tests that read like natural language descriptions of behavior. ## When to Use BDD Syntax **Use BDD (`describe`/`it`) when:** - Documenting intended behavior of new features - Testing complex components with multiple related facets - Following test-first development workflows - Tests serve as specification documentation - You want hierarchical organization of related tests - A group of tests (in `it()` statements) rely on a single fixture or local options/envvars (set up in `describe()`) **Use standard syntax (`test_that`) when:** - Writing straightforward unit tests - Testing implementation details - Converting existing test_that() tests (no need to change working code) **Key insight from testthat:** "Use `describe()` to verify you implement the right things, use `test_that()` to ensure you do things right." ## Basic BDD Syntax ### Simple Specifications ```r 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)) }) it("can compute determinant", { m <- matrix(c(1, 2, 3, 4), 2, 2) expect_equal(det(m), -2) }) }) ``` Each `it()` block: - Defines one specification (like `test_that()`) - Runs in its own environment - Has access to all expectations - Can use withr and other testing tools ## Nested Specifications Group related specifications hierarchically: ```r describe("User authentication", { describe("login()", { it("accepts valid credentials", { result <- login("user@example.com", "password123") expect_true(result$authenticated) expect_type(result$token, "character") }) it("rejects invalid email", { expect_error( login("invalid-email", "password"), class = "validation_error" ) }) it("rejects wrong password", { expect_error( login("user@example.com", "wrong"), class = "auth_error" ) }) }) describe("logout()", { it("clears session token", { session <- create_session() logout(session) expect_null(session$token) }) it("invalidates refresh token", { session <- create_session() logout(session) expect_error(refresh(session), "Invalid token") }) }) describe("password_reset()", { it("sends reset email", { local_mocked_bindings(send_email = function(...) TRUE) result <- password_reset("user@example.com") expect_true(result$email_sent) }) it("generates secure token", { result <- password_reset("user@example.com") expect_gte(nchar(result$token), 32) }) }) }) ``` Nesting creates clear hierarchies: - Top level: Component or module - Second level: Functions or features - Third level: Specific behaviors ## Pending Specifications Mark unimplemented tests by omitting the code: ```r describe("division()", { it("divides two numbers", { expect_equal(division(10, 2), 5) }) it("returns Inf for division by zero") # Pending it("handles complex numbers") # Pending }) ``` Pending tests: - Show up in test output as "SKIPPED" - Document planned functionality - Serve as TODO markers - Don't cause test failures ## Complete Test File Example ```r # tests/testthat/test-data-processor.R describe("DataProcessor", { describe("initialization", { it("creates processor with default config", { proc <- DataProcessor$new() expect_r6_class(proc, "DataProcessor") expect_equal(proc$config$timeout, 30) }) it("accepts custom configuration", { proc <- DataProcessor$new(config = list(timeout = 60)) expect_equal(proc$config$timeout, 60) }) it("validates configuration options", { expect_error( DataProcessor$new(config = list(timeout = -1)), "timeout must be positive" ) }) }) describe("process()", { describe("with valid data", { it("processes numeric data", { proc <- DataProcessor$new() result <- proc$process(data.frame(x = 1:10)) expect_s3_class(result, "data.frame") expect_equal(nrow(result), 10) }) it("handles missing values", { proc <- DataProcessor$new() data <- data.frame(x = c(1, NA, 3)) result <- proc$process(data) expect_false(anyNA(result$x)) }) it("preserves column names", { proc <- DataProcessor$new() data <- data.frame(foo = 1:3, bar = 4:6) result <- proc$process(data) expect_named(result, c("foo", "bar")) }) }) describe("with invalid data", { it("rejects NULL input", { proc <- DataProcessor$new() expect_error(proc$process(NULL), "data cannot be NULL") }) it("rejects empty data frame", { proc <- DataProcessor$new() expect_error(proc$process(data.frame()), "data cannot be empty") }) it("rejects non-data.frame input", { proc <- DataProcessor$new() expect_error(proc$process(list()), class = "type_error") }) }) }) describe("summary()", { it("returns summary statistics", { proc <- DataProcessor$new() data <- data.frame(x = 1:10, y = 11:20) proc$process(data) summary <- proc$summary() expect_type(summary, "list") expect_named(summary, c("rows", "cols", "processed_at")) }) it("throws error if no data processed", { proc <- DataProcessor$new() expect_error(proc$summary(), "No data has been processed") }) }) }) ``` ## Organizing Files with BDD ### Single Component per File ```r # tests/testthat/test-user-model.R describe("User model", { describe("validation", { ... }) describe("persistence", { ... }) describe("relationships", { ... }) }) ``` ### Multiple Related Components ```r # tests/testthat/test-math-operations.R describe("arithmetic operations", { describe("addition()", { ... }) describe("subtraction()", { ... }) describe("multiplication()", { ... }) describe("division()", { ... }) }) ``` ### Hierarchical Domain Organization ```r # tests/testthat/test-api-endpoints.R describe("API endpoints", { describe("/users", { describe("GET /users", { ... }) describe("POST /users", { ... }) describe("GET /users/:id", { ... }) }) describe("/posts", { describe("GET /posts", { ... }) describe("POST /posts", { ... }) }) }) ``` ## Mixing BDD and Standard Syntax You can use both styles in the same test file: ```r # tests/testthat/test-calculator.R # BDD style for user-facing functionality describe("Calculator user interface", { describe("button clicks", { it("registers numeric input", { ... }) it("handles operator keys", { ... }) }) }) # Standard style for internal helpers test_that("parse_expression() tokenizes correctly", { tokens <- parse_expression("2 + 3") expect_equal(tokens, c("2", "+", "3")) }) test_that("evaluate_tokens() handles operator precedence", { result <- evaluate_tokens(c("2", "+", "3", "*", "4")) expect_equal(result, 14) }) ``` **Mixing guidelines:** - Use BDD for behavioral specifications - Use `test_that()` for implementation details - Keep related tests in the same style within a section - Don't nest `test_that()` inside `describe()` or vice versa ## BDD with Test Fixtures Use the same fixture patterns as standard tests: ```r describe("File processor", { # Helper function for tests new_test_file <- function(content) { path <- withr::local_tempfile(lines = content) path } describe("read_file()", { it("reads text files", { file <- new_test_file(c("line1", "line2")) result <- read_file(file) expect_length(result, 2) }) it("handles empty files", { file <- new_test_file(character()) result <- read_file(file) expect_equal(result, character()) }) }) }) ``` ## BDD with Snapshot Tests Snapshots work naturally with BDD: ```r describe("error messages", { it("provides helpful validation errors", { expect_snapshot(error = TRUE, { validate_user(NULL) validate_user(list()) validate_user(list(email = "invalid")) }) }) it("shows clear permission errors", { expect_snapshot(error = TRUE, { check_permission("guest", "admin") }) }) }) ``` ## BDD with Mocking ```r describe("API client", { describe("fetch_user()", { it("handles successful response", { local_mocked_bindings( http_get = function(url) { list(status = 200, body = '{"id": 1, "name": "Test"}') } ) user <- fetch_user(1) expect_equal(user$name, "Test") }) it("handles 404 errors", { local_mocked_bindings( http_get = function(url) list(status = 404) ) expect_error(fetch_user(999), class = "not_found_error") }) }) }) ``` ## Test-First Workflow with BDD 1. **Write specifications first:** ```r describe("order_total()", { it("sums item prices") it("applies tax") it("applies discount codes") it("handles free shipping threshold") }) ``` 2. **Implement one specification at a time:** ```r describe("order_total()", { it("sums item prices", { order <- list(items = list( list(price = 10), list(price = 20) )) expect_equal(order_total(order), 30) }) it("applies tax") it("applies discount codes") it("handles free shipping threshold") }) ``` 3. **Continue until all specs have implementations** This workflow ensures you: - Think about requirements before implementation - Have clear success criteria - Build incrementally - Maintain focus on behavior ## Comparison: describe/it vs test_that **describe/it:** ```r describe("str_length()", { it("counts characters in string", { expect_equal(str_length("abc"), 3) }) it("handles empty strings", { expect_equal(str_length(""), 0) }) }) ``` **test_that:** ```r test_that("str_length() counts characters", { expect_equal(str_length("abc"), 3) }) test_that("str_length() handles empty strings", { expect_equal(str_length(""), 0) }) ``` Key differences: - BDD groups related specs under `describe()` - BDD uses "it" instead of "test_that" - BDD enables nesting for hierarchy - BDD supports pending specs without code - Both produce identical test results Choose based on your preferences and project style. -
fixtures.md 7.4 KB
# Test Fixtures and Data Management Test fixtures arrange the environment into a known state for testing. testthat provides several approaches for managing test data and state. ## Fixture Approaches ### Constructor Helper Functions Create reusable test objects on-demand: ```r # In tests/testthat/helper-fixtures.R or within test file new_sample_data <- function(n = 10) { data.frame( id = seq_len(n), value = rnorm(n), category = sample(letters[1:3], n, replace = TRUE) ) } test_that("function handles data correctly", { data <- new_sample_data(5) result <- process_data(data) expect_equal(nrow(result), 5) }) ``` **Advantages:** - Fresh data for each test - Parameterizable - No file I/O **Use when:** - Data is cheap to create - Multiple tests need similar but not identical data - Data should vary between tests ### Local Functions with Cleanup Handle side effects using `withr::defer()`: ```r local_temp_csv <- function(data, pattern = "test", env = parent.frame()) { path <- withr::local_tempfile(pattern = pattern, fileext = ".csv", .local_envir = env) write.csv(data, path, row.names = FALSE) path } test_that("CSV reading works", { data <- data.frame(x = 1:3, y = letters[1:3]) csv_path <- local_temp_csv(data) result <- read_my_csv(csv_path) expect_equal(result, data) # File automatically cleaned up after test }) ``` **Advantages:** - Automatic cleanup - Encapsulates setup and teardown - Composable **Use when:** - Tests create side effects (files, connections) - Setup requires multiple steps - Cleanup logic is non-trivial ### Static Fixture Files Store pre-created data files in `tests/testthat/fixtures/`: ``` tests/testthat/ ├── fixtures/ │ ├── sample_data.rds │ ├── config.json │ └── reference_output.csv └── test-processing.R ``` Access with `test_path()`: ```r test_that("function processes real data", { data <- readRDS(test_path("fixtures", "sample_data.rds")) result <- process_data(data) expected <- readRDS(test_path("fixtures", "expected_output.rds")) expect_equal(result, expected) }) ``` **Advantages:** - Tests against real data - Expensive-to-create data computed once - Human-readable (for JSON, CSV, etc.) **Use when:** - Data is expensive to create - Data represents real-world cases - Multiple tests use identical data - Data is complex or represents edge cases ## Helper Files Files in `tests/testthat/` starting with `helper-` are automatically sourced before tests run. ```r # tests/testthat/helper-fixtures.R # Custom expectations expect_valid_user <- function(user) { expect_type(user, "list") expect_named(user, c("id", "name", "email")) expect_type(user$id, "integer") } # Test data constructors new_user <- function(id = 1L, name = "Test User", email = "test@example.com") { list(id = id, name = name, email = email) } # Shared fixtures standard_config <- function() { list( timeout = 30, retries = 3, verbose = FALSE ) } ``` Then use in tests: ```r test_that("user validation works", { user <- new_user() expect_valid_user(user) }) ``` ## Setup Files Files starting with `setup-` run only during `R CMD check` and `devtools::test()`, not during `devtools::load_all()`. ```r # tests/testthat/setup-options.R # Set options for test suite withr::local_options( list( reprex.clipboard = FALSE, reprex.html_preview = FALSE, usethis.quiet = TRUE ), .local_envir = teardown_env() ) ``` **Use setup files for:** - Package-wide test options - Environment variable configuration - One-time expensive operations - Test suite initialization ## Managing File System State ### Use temp directories exclusively ```r test_that("file writing works", { # Good: write to temp directory path <- withr::local_tempfile(lines = c("line1", "line2")) # Bad: write to working directory # writeLines(c("line1", "line2"), "test_file.txt") result <- process_file(path) expect_equal(result, expected) }) ``` ### Clean up automatically with withr ```r test_that("directory operations work", { # Create temp dir that auto-cleans dir <- withr::local_tempdir() # Create files in it file.create(file.path(dir, "file1.txt")) file.create(file.path(dir, "file2.txt")) result <- process_directory(dir) expect_length(result, 2) # Directory automatically removed after test }) ``` ### Test files stored in fixtures ```r test_that("file parsing handles edge cases", { # Read from committed fixture malformed <- test_path("fixtures", "malformed.csv") expect_warning( result <- robust_read_csv(malformed), "Malformed" ) expect_true(nrow(result) > 0) }) ``` ## Database Fixtures ### In-memory SQLite ```r test_that("database queries work", { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") withr::defer(DBI::dbDisconnect(con)) # Create schema DBI::dbExecute(con, "CREATE TABLE users (id INTEGER, name TEXT)") DBI::dbExecute(con, "INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')") result <- query_users(con) expect_equal(nrow(result), 2) }) ``` ### Fixture SQL scripts ```r test_that("complex queries work", { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") withr::defer(DBI::dbDisconnect(con)) # Load schema from fixture schema <- readLines(test_path("fixtures", "schema.sql")) DBI::dbExecute(con, paste(schema, collapse = "\n")) result <- complex_query(con) expect_s3_class(result, "data.frame") }) ``` ## Complex Object Fixtures ### Save and load complex objects Create fixtures interactively: ```r # Run once to create fixture complex_model <- expensive_training_function(data) saveRDS(complex_model, "tests/testthat/fixtures/trained_model.rds") ``` Use in tests: ```r test_that("predictions work", { model <- readRDS(test_path("fixtures", "trained_model.rds")) new_data <- data.frame(x = 1:5, y = 6:10) predictions <- predict(model, new_data) expect_length(predictions, 5) expect_type(predictions, "double") }) ``` ## Fixture Organization ``` tests/testthat/ ├── fixtures/ │ ├── data/ # Input data │ │ ├── sample.csv │ │ └── users.json │ ├── expected/ # Expected outputs │ │ ├── processed.rds │ │ └── summary.txt │ ├── models/ # Trained models │ │ └── classifier.rds │ └── sql/ # Database schemas │ └── schema.sql ├── helper-constructors.R # Data constructors ├── helper-expectations.R # Custom expectations ├── setup-options.R # Test suite config └── test-*.R # Test files ``` ## Best Practices **Keep fixtures small:** - Store minimal data needed for tests - Use constructors for variations - Commit fixtures to version control **Document fixture origins:** ```r # tests/testthat/fixtures/README.md # sample_data.rds Created from production data on 2024-01-15 Contains 100 representative records with PII removed # malformed.csv Edge case discovered in issue #123 Contains intentional formatting errors ``` **Use consistent paths:** ```r # Always use test_path() for portability data <- readRDS(test_path("fixtures", "data.rds")) # Never use relative paths # data <- readRDS("fixtures/data.rds") # Bad ``` **Prefer deterministic fixtures:** ```r # Good: reproducible set.seed(123) data <- data.frame(x = rnorm(10)) # Better: no randomness data <- data.frame(x = seq(-2, 2, length.out = 10)) ``` -
mocking.md 5 KB
# Mocking in testthat Mocking temporarily replaces function implementations during testing, enabling tests when dependencies are unavailable or impractical (databases, APIs, file systems, expensive computations). ## Core Mocking Functions ### `local_mocked_bindings()` Replace function implementations within a test: ```r test_that("function works with mocked dependency", { local_mocked_bindings( get_user_data = function(id) { list(id = id, name = "Test User", role = "admin") } ) result <- process_user(123) expect_equal(result$name, "Test User") }) ``` ### `with_mocked_bindings()` Replace functions for a specific code block: ```r test_that("handles API failures gracefully", { result <- with_mocked_bindings( api_call = function(...) stop("Network error"), { tryCatch( fetch_data(), error = function(e) "fallback" ) } ) expect_equal(result, "fallback") }) ``` ## S3 Method Mocking Use `local_mocked_s3_method()` to mock S3 methods: ```r test_that("custom print method is used", { local_mocked_s3_method( print, "myclass", function(x, ...) cat("Mocked output\n") ) obj <- structure(list(), class = "myclass") expect_output(print(obj), "Mocked output") }) ``` ## S4 Method Mocking Use `local_mocked_s4_method()` for S4 methods: ```r test_that("S4 method override works", { local_mocked_s4_method( "show", "MyS4Class", function(object) cat("Mocked S4 output\n") ) # Test code using the mocked method }) ``` ## R6 Class Mocking Use `local_mocked_r6_class()` to mock R6 classes: ```r test_that("R6 mock works", { MockDatabase <- R6::R6Class("MockDatabase", public = list( query = function(sql) data.frame(result = "mocked") ) ) local_mocked_r6_class("Database", MockDatabase) db <- Database$new() expect_equal(db$query("SELECT *"), data.frame(result = "mocked")) }) ``` ## Common Mocking Patterns ### Database Connections ```r test_that("database queries work", { local_mocked_bindings( dbConnect = function(...) list(connected = TRUE), dbGetQuery = function(conn, sql) { data.frame(id = 1:3, value = c("a", "b", "c")) } ) result <- fetch_from_db("SELECT * FROM table") expect_equal(nrow(result), 3) }) ``` ### API Calls ```r test_that("API integration works", { local_mocked_bindings( httr2::request = function(url) list(url = url), httr2::req_perform = function(req) { list(status_code = 200, content = '{"success": true}') } ) result <- call_external_api() expect_true(result$success) }) ``` ### File System Operations ```r test_that("file processing works", { local_mocked_bindings( file.exists = function(path) TRUE, readLines = function(path) c("line1", "line2", "line3") ) result <- process_file("dummy.txt") expect_length(result, 3) }) ``` ### Random Number Generation ```r test_that("randomized algorithm is deterministic", { local_mocked_bindings( runif = function(n, ...) rep(0.5, n), rnorm = function(n, ...) rep(0, n) ) result <- randomized_function() expect_equal(result, expected_value) }) ``` ## Advanced Mocking Packages ### webfakes Create fake web servers for HTTP testing: ```r test_that("API client handles responses", { app <- webfakes::new_app() app$get("/users/:id", function(req, res) { res$send_json(list(id = req$params$id, name = "Test")) }) web <- webfakes::local_app_process(app) result <- get_user(web$url("/users/123")) expect_equal(result$name, "Test") }) ``` ### httptest2 Record and replay HTTP interactions: ```r test_that("API call returns expected data", { httptest2::with_mock_dir("fixtures/api", { result <- call_real_api() expect_equal(result$status, "success") }) }) ``` First run records real responses; subsequent runs replay them. ## Mocking Best Practices **Mock at the right level:** - Mock external dependencies (APIs, databases) - Don't mock internal package functions excessively - Keep mocks simple and focused **Verify mock behavior:** ```r test_that("mock is called correctly", { calls <- list() local_mocked_bindings( external_func = function(...) { calls <<- append(calls, list(list(...))) "mocked" } ) my_function() expect_length(calls, 1) expect_equal(calls[[1]]$arg, "expected") }) ``` **Prefer real fixtures when possible:** - Use test data files instead of mocking file reads - Use webfakes for full HTTP testing instead of mocking individual functions - Mock only when fixtures are impractical **Document what's being mocked:** ```r test_that("handles unavailable service", { # Mock the external authentication service # which is unavailable in test environment local_mocked_bindings( auth_check = function(token) list(valid = TRUE) ) # test code }) ``` ## Migration from Deprecated Functions **Old (deprecated):** ```r with_mock( pkg::func = function(...) "mocked" ) ``` **New (recommended):** ```r local_mocked_bindings( func = function(...) "mocked", .package = "pkg" ) ``` The new functions work with byte-compiled code and are more reliable across platforms. -
snapshots.md 3.6 KB
# Snapshot Testing Snapshot tests record expected output in human-readable files rather than inline code. They are ideal for: - Complex output that's difficult to verify programmatically - User-facing messages, warnings, and errors - Mixed output types (printed text + messages + warnings) - Binary formats like plots - Text with complex formatting ## Basic Usage ```r test_that("error messages are helpful", { expect_snapshot( my_function(bad_input) ) }) ``` The first run creates `tests/testthat/_snaps/{test-file}/{test-name}.md` containing the captured output. ## Snapshot Workflow **Initial creation:** ```r devtools::test() # Creates new snapshots ``` **Review changes:** ```r testthat::snapshot_review('test-name') ``` **Accept changes:** ```r testthat::snapshot_accept('test-name') ``` **Reject changes:** ```r testthat::snapshot_reject('test-name') ``` **Download snapshots from GitHub CI:** ```r testthat::snapshot_download_gh() ``` ## Snapshot Types ### Output Snapshots Capture printed output, messages, warnings, and errors: ```r test_that("function produces expected output", { expect_snapshot({ print(my_data) message("Processing complete") warning("Non-critical issue") }) }) ``` ### Value Snapshots Capture the structure of R objects: ```r test_that("data structure is correct", { expect_snapshot(str(complex_object)) }) ``` ### Error Snapshots Capture error messages with call information: ```r test_that("errors are informative", { expect_snapshot( error = TRUE, my_function(invalid_input) ) }) ``` ## Transform Function Use `transform` to remove variable elements before comparison: ```r test_that("output is stable", { expect_snapshot( my_api_call(), transform = function(lines) { # Remove timestamps gsub("\\d{4}-\\d{2}-\\d{2}", "[DATE]", lines) } ) }) ``` Common uses: - Remove timestamps or session IDs - Normalize file paths - Strip API keys or tokens - Remove stochastic elements ## Variants Use `variant` for platform-specific or R-version-specific snapshots: ```r test_that("platform-specific behavior", { expect_snapshot( system_specific_function(), variant = tolower(Sys.info()[["sysname"]]) ) }) ``` Variants save to `_snaps/{variant}/{test}.md` instead of `_snaps/{test}.md`. ## Best Practices - **Commit snapshots to git** - They are part of your test suite - **Review snapshot diffs carefully** - Ensure changes are intentional - **Keep snapshots focused** - One concept per snapshot - **Use transform for stability** - Remove variable elements - **Update snapshots explicitly** - Never auto-accept in CI - **Fail on new snapshots in CI** - testthat 3.3.0+ does this automatically ## Snapshot Files Snapshots are stored as markdown files in `tests/testthat/_snaps/`: ``` tests/testthat/ ├── test-utils.R └── _snaps/ ├── test-utils.md └── windows/ # variant snapshots └── test-utils.md ``` Each snapshot includes: - Test name as heading - Code that generated the output - Captured output ## Common Patterns **Testing error messages:** ```r test_that("validation errors are clear", { expect_snapshot(error = TRUE, { validate_input(NULL) validate_input("wrong type") validate_input(numeric()) }) }) ``` **Testing side-by-side comparisons:** ```r test_that("diff output is readable", { withr::local_options(width = 80) expect_snapshot( waldo::compare(expected, actual) ) }) ``` **Testing printed output with messages:** ```r test_that("function provides feedback", { expect_snapshot({ result <- process_data(sample_data) print(result) }) }) ```
-
-
SKILL.md 10.9 KB
--- name: testing-r-packages description: Best practices for writing R package tests using testthat version 3+. Use when writing, organizing, or improving tests for R packages. Covers test structure, expectations, fixtures, snapshots, mocking, and modern testthat 3 patterns including self-sufficient tests, proper cleanup with withr, and snapshot testing. metadata: author: Garrick Aden-Buie (@gadenbuie) version: "1.1" license: MIT --- # Testing R Packages with testthat Modern best practices for R package testing using testthat 3+. ## Initial Setup Initialize testing with testthat 3rd edition: ```r 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 ```r 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()`: ```r 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](references/bdd.md) for comprehensive BDD patterns, nested specifications, and test-first workflows. ## Running Tests Three scales of testing: **Micro** (interactive development): ```r devtools::load_all() expect_equal(foofy(...), expected) ``` **Mezzo** (single file): ```r testthat::test_file("tests/testthat/test-foofy.R") # RStudio: Ctrl/Cmd + Shift + T ``` **Macro** (full suite): ```r devtools::test() # Ctrl/Cmd + Shift + T devtools::check() # Ctrl/Cmd + Shift + E ``` ## Core Expectations ### Equality ```r expect_equal(10, 10 + 1e-7) # Allows numeric tolerance expect_identical(10L, 10L) # Exact match required expect_all_equal(x, expected) # Every element matches (v3.3.0+) ``` ### Errors, Warnings, Messages ```r 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 ```r expect_match("Testing is fun!", "Testing") expect_match(text, "pattern", ignore.case = TRUE) ``` ### Structure and Type ```r expect_length(vector, 10) expect_type(obj, "list") expect_s3_class(model, "lm") expect_s4_class(obj, "MyS4Class") expect_r6_class(obj, "MyR6Class") # v3.3.0+ expect_shape(matrix, c(10, 5)) # v3.3.0+ ``` ### Sets and Collections ```r expect_setequal(x, y) # Same elements, any order expect_contains(fruits, "apple") # Subset check (v3.2.0+) expect_in("apple", fruits) # Element in set (v3.2.0+) expect_disjoint(set1, set2) # No overlap (v3.3.0+) ``` ### Logical ```r expect_true(condition) expect_false(condition) expect_all_true(vector > 0) # All elements TRUE (v3.3.0+) expect_all_false(vector < 0) # All elements FALSE (v3.3.0+) ``` ## Design Principles ### 1. Self-Sufficient Tests Each test should contain all setup, execution, and teardown code: ```r # Good: self-contained test_that("foofy() works", { data <- data.frame(x = 1:3, y = letters[1:3]) result <- foofy(data) expect_equal(result$x, 1:3) }) # Bad: relies on ambient state dat <- data.frame(x = 1:3, y = letters[1:3]) test_that("foofy() works", { result <- foofy(dat) # Where did 'dat' come from? expect_equal(result$x, 1:3) }) ``` ### 2. Self-Contained Tests (Cleanup Side Effects) Use `withr` to manage state changes: ```r 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") # Automatic cleanup after test }) ``` **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](references/snapshots.md) for complete guide. **Basic pattern:** ```r test_that("error message is helpful", { expect_snapshot( error = TRUE, validate_input(NULL) ) }) ``` Snapshots stored in `tests/testthat/_snaps/`. **Workflow:** ```r devtools::test() # Creates new snapshots testthat::snapshot_review('name') # Review changes testthat::snapshot_accept('name') # Accept changes ``` ## Test Fixtures and Data Three approaches for test data: **1. Constructor functions** - Create data on-demand: ```r new_sample_data <- function(n = 10) { data.frame(id = seq_len(n), value = rnorm(n)) } ``` **2. Local functions with cleanup** - Handle side effects: ```r local_temp_csv <- function(data, env = parent.frame()) { path <- withr::local_tempfile(fileext = ".csv", .local_envir = env) write.csv(data, path, row.names = FALSE) path } ``` **3. Static fixture files** - Store in `fixtures/` directory: ```r data <- readRDS(test_path("fixtures", "sample_data.rds")) ``` See [references/fixtures.md](references/fixtures.md) for detailed fixture patterns. ## Mocking Replace external dependencies during testing using `local_mocked_bindings()`. See [references/mocking.md](references/mocking.md) for comprehensive mocking strategies. **Basic pattern:** ```r test_that("function works with mocked dependency", { local_mocked_bindings( external_api = function(...) list(status = "success", data = "mocked") ) result <- my_function_that_calls_api() expect_equal(result$status, "success") }) ``` ## Common Patterns ### Testing Errors with Specific Classes ```r test_that("validation catches errors", { expect_error( validate_input("wrong_type"), class = "vctrs_error_cast" ) }) ``` ### Testing with Temporary Files ```r test_that("file processing works", { temp_file <- withr::local_tempfile( lines = c("line1", "line2", "line3") ) result <- process_file(temp_file) expect_equal(length(result), 3) }) ``` ### Testing with Modified Options ```r test_that("output respects width", { withr::local_options(width = 40) output <- capture_output(print(my_object)) expect_lte(max(nchar(strsplit(output, "\n")[[1]])), 40) }) ``` ### Testing Multiple Related Cases ```r test_that("str_trunc() handles all directions", { trunc <- function(direction) { str_trunc("This string is moderately long", direction, width = 20) } expect_equal(trunc("right"), "This string is mo...") expect_equal(trunc("left"), "...erately long") expect_equal(trunc("center"), "This stri...ely long") }) ``` ### Custom Expectations in Helper Files ```r # In tests/testthat/helper-expectations.R expect_valid_user <- function(user) { expect_type(user, "list") expect_named(user, c("id", "name", "email")) expect_type(user$id, "integer") expect_match(user$email, "@") } # In test file test_that("user creation works", { user <- create_user("test@example.com") expect_valid_user(user) }) ``` ## File System Discipline **Always write to temp directory:** ```r # Good output <- withr::local_tempfile(fileext = ".csv") write.csv(data, output) # Bad - writes to package directory write.csv(data, "output.csv") ``` **Access test fixtures with `test_path()`:** ```r # Good - works in all contexts data <- readRDS(test_path("fixtures", "data.rds")) # Bad - relative paths break data <- readRDS("fixtures/data.rds") ``` ## Advanced Topics For advanced testing scenarios, see: - **[references/bdd.md](references/bdd.md)** - BDD-style testing with describe/it, nested specifications, test-first workflows - **[references/snapshots.md](references/snapshots.md)** - Snapshot testing, transforms, variants - **[references/mocking.md](references/mocking.md)** - Mocking strategies, webfakes, httptest2 - **[references/fixtures.md](references/fixtures.md)** - Fixture patterns, database fixtures, helper files - **[references/advanced.md](references/advanced.md)** - Skipping tests, secrets management, CRAN requirements, custom expectations, parallel testing ## testthat 3 Modernizations When working with testthat 3 code, prefer modern patterns: **Deprecated → Modern:** - `context()` → Remove (duplicates filename) - `expect_equivalent()` → `expect_equal(ignore_attr = TRUE)` - `with_mock()` → `local_mocked_bindings()` - `is_null()`, `is_true()`, `is_false()` → `expect_null()`, `expect_true()`, `expect_false()` **New in testthat 3:** - Edition system (`Config/testthat/edition: 3`) - Improved snapshot testing - `waldo::compare()` for better diff output - Unified condition handling - `local_mocked_bindings()` works with byte-compiled code - Parallel test execution support ## Quick Reference **Initialize:** `usethis::use_testthat(3)` **Run tests:** `devtools::test()` or Ctrl/Cmd + Shift + T **Create test file:** `usethis::use_test("name")` **Review snapshots:** `testthat::snapshot_review()` **Accept snapshots:** `testthat::snapshot_accept()` **Find slow tests:** `devtools::test(reporter = "slow")` **Shuffle tests:** `devtools::test(shuffle = TRUE)`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.