Claude Skill

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

LLM Mart · 0 points · 20 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download posit-dev-skills-r-lib_testing-r-packages-b58a92e.zip · 17 KB
Part of posit-dev/skills — 23 skills

Install

skills CLI npx skills add https://github.com/posit-dev/skills/tree/main/r-lib/testing-r-packages
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install posit-dev-skills@llmmart
Git 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 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

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 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 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 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 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:

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.

No comments yet.

Reviews (0)

No reviews yet.

Related