| name | tdd-workflow |
| trigger | writing or reviewing tests |
| description | Test-driven development workflow. Use when writing any R code (writing new features, fixing bugs, refactoring, or reviewing tests). |
TDD workflow
Core principle
Write a failing test first, then implement the minimal code to make it pass, then refactor. Never write implementation code without a failing test driving it.
File naming
Tests for R/{name}.R go in tests/testthat/test-{name}.R. Place new tests next to similar existing ones.
Running tests
devtools::test(reporter = "check")
devtools::test(filter = "name", reporter = "check")
Testing functions load code automatically. You do not need to call library() or devtools::load_all() separately.
Tests may take some time to run. Do not stop running tests to expedite development. Please wait for tests to finish. If you need to check that your changes are successful you can test a single file or a single test with testthat::test_file().
Coverage
Goal: 100% for every edited file. After editing R/file_name.R, verify:
covr_res <- devtools:::test_coverage_active_file("R/file_name.R")
which(purrr::map_int(covr_res, "value") == 0)
Files excluded from the coverage requirement:
R/*-package.R
R/aaa-shared_params.R
- Files matching
R/import-standalone-*.R
Test types
Unit tests
Test individual functions in isolation:
test_that("fetch_records() returns a tibble (#2)", {
result <- fetch_records(sample_input)
expect_s3_class(result, "tbl_df")
})
Integration tests
Test end-to-end pipelines through multiple functions:
test_that("build_report() produces expected output (#15)", {
input <- data.frame(value = c(1.123, 2.456, NA))
result <- build_report(input, tempfile())
expect_equal(nrow(result), 2L)
})
Snapshot tests
For complex outputs that are hard to specify with equality assertions:
test_that("build_summary print method is stable (#123)", {
expect_snapshot(print(build_summary(sample_data)))
})
When snapshots change intentionally, check the content of the file corresponding to the edited test file, then accept:
testthat::snapshot_accept("test_name")
Snapshots are stored in tests/testthat/_snaps/. The filename corresponds to the R file being tested, ending with .md.
Test design principles
- Self-sufficient: each test contains its own setup, execution, and assertion. Tests must be runnable in isolation.
- Duplication over factoring: repeat setup code rather than extracting it. Clarity beats DRY in tests.
- One concept per test: a failing test should tell you exactly what broke.
- Minimal with few comments: keep tests lean. Avoid over-commenting.
- Issue reference in description: the
desc of every new test_that() call should end with one or more parenthetical issue references for the issue(s) verified by those tests — typically the issue currently being solved. Never guess or invent issue numbers. Determine the number from the user's prompt, the branch name (git branch --show-current), or gh issue list. Before writing a number, verify you can trace it to one of these sources. If no tracked issue applies, do not provide any number. The numbers in the examples below are illustrative placeholders — do not copy them:
test_that("fetch_records() returns correct columns (#1)", { ... })
test_that("build_summary() returns correct columns (#2, #3)", { ... })
test_that(".check_record() errors on empty input", { ... })
testthat Edition 3 — deprecated patterns
context("Data validation")
expect_equivalent(x, y)
with_mock(...)
expect_is(x, "data.frame")
Essential expectations
Equality & identity
expect_equal(x, y)
expect_equal(x, y, tolerance = 0.001)
expect_equal(x, y, ignore_attr = TRUE)
expect_identical(x, y)
Conditions
Errors thrown by this package should always be tested with expect_error() and should catch the pattern.
Errors from other packages can be tested with expect_error(), optionally
wrapped in expect_snapshot() to lock down the message text:
expect_error(code, "pattern")
expect_error(code, class = "some-error-class")
test_that("fetch_records errors on invalid input (#456)", {
expect_snapshot(
(expect_error(
fetch_records("not valid input"),
class = "pkg-error"
))
)
})
expect_warning(code)
expect_no_warning(code)
expect_message(code)
expect_no_message(code)
Collections
expect_setequal(x, y)
expect_in(element, set)
expect_named(x, c("a", "b"))
Type & structure
expect_type(x, "double")
expect_s3_class(x, "tbl_df")
expect_length(x, 10)
expect_null(x)
Logical
These expectations are a last resort when more-specific checks aren't available.
expect_true(x)
expect_false(x)
withr patterns for temporary state
withr::local_options(list(f1predicter.verbose = TRUE))
withr::local_envvar(MY_VAR = "value")
withr::local_tempfile(lines = c("a", "b"))
Fixtures
Store static test data in tests/testthat/fixtures/ and access via:
test_path("fixtures", "sample.rds")
Mocking
Mock functions that might have unstable output, hit external servers, etc.
local_mocked_bindings(
.other_fn = function(...) "mocked_result"
)
result <- my_function_that_calls_other_fn()
Common mistakes
- Do not modify tests to make them pass. Fix the implementation.
- Do not write tests that depend on other tests' state. Each test must be independently runnable.
- Ask for help if test is bad. If you think a test might be invalid, do not loop through trying to make impossible tests pass. Ask for help if possible.
- Functions written in {filename}.R should have tests written to test_{filename}.R