Why Automated Testing Matters in R
Manually re-running a script and eyeballing the output after every change doesn't scale past a handful of functions, and it's exactly how subtle regressions slip into production — a function that used to handle empty vectors correctly quietly breaks six months later when someone refactors an unrelated part of the codebase. testthat formalizes this by letting you write assertions once, as expect_*() calls inside a test_that() block, and then re-run the entire suite in seconds any time code changes, catching regressions before a colleague or a CRAN reviewer does.
Cricket analogy: Manually eyeballing output after every code change is like a coach judging a bowler's action by eye every match instead of running it through the same biomechanics test each time — testthat is that repeatable, objective test.
Writing Your First Tests
A test file lives under tests/testthat/ and is named test-*.R to match the function file it covers; inside, a test_that() call groups related expect_*() assertions under a human-readable description that prints in the test output when something fails, such as test_that("add() sums two numbers", { expect_equal(add(2, 3), 5) }). expect_error(risky_function(-1), "must be positive") verifies that invalid input triggers the right error message, which is just as important to test as the happy path, since a function that fails silently on bad input is often more dangerous than one that fails loudly.
Cricket analogy: Grouping assertions under a human-readable test_that() description is like a match report labeling each incident clearly — 'no-ball review,' 'DRS overturned' — so when something goes wrong, the description alone tells you exactly what failed.
Common Expectation Functions
expect_equal() allows a small numeric tolerance (useful for floating-point comparisons) and compares values loosely, while expect_identical() requires an exact match including attributes and type, which matters when testing that a function preserves an object's class or names, not just its numeric content. expect_type() checks the underlying R type ("double", "character", "list"), expect_s3_class()/expect_s4_class() verify object-oriented class membership, and expect_true()/expect_false() handle arbitrary logical conditions that don't fit a more specific expectation.
Cricket analogy: expect_equal()'s floating-point tolerance is like a run-rate calculation accepting 8.666... as 'close enough' to 8.67 for practical purposes, while expect_identical() is like an exact ball-by-ball replay requiring every single delivery to match precisely.
# tests/testthat/test-customer_ltv.R
test_that("customer_ltv computes expected value for typical input", {
result <- customer_ltv(avg_order_value = 50, purchase_frequency = 4, retention_years = 2)
expect_equal(result, 400)
expect_type(result, "double")
})
test_that("customer_ltv rejects non-positive order value", {
expect_error(
customer_ltv(avg_order_value = -10, purchase_frequency = 4, retention_years = 2),
"must be positive"
)
})
test_that("customer_ltv handles fractional frequency correctly", {
result <- customer_ltv(avg_order_value = 30, purchase_frequency = 2.5, retention_years = 1)
expect_equal(result, 75, tolerance = 1e-6)
})testthat's 3rd edition (enabled with usethis::use_testthat(3)) adds snapshot testing via expect_snapshot(), which records a function's full output — including printed messages, not just return values — to a reference file on first run, then flags any difference on subsequent runs. This is especially useful for testing print methods, error messages, and complex nested output where writing an exact expect_equal() comparison would be tedious.
Organizing and Running Tests
Inside a package, tests live under tests/testthat/, one test-*.R file per source file being tested, and tests/testthat.R (auto-generated by usethis::use_testthat()) is the single entry point that R CMD check calls to run them all. During development, devtools::test() runs the full suite from the console, devtools::test_file("tests/testthat/test-customer_ltv.R") runs just one file while iterating on a specific function, and wiring the same check into a GitHub Actions workflow (via usethis::use_github_action("check-standard")) means every pull request runs the suite automatically before merge.
Cricket analogy: Running devtools::test_file() on just one file while iterating mirrors a bowler doing focused net practice on a single delivery type, like yorkers, rather than running a full net session every time they tweak their action.
Tests must be independent and order-agnostic — a test_that() block that depends on a variable or file created by an earlier test will pass in sequence but fail (or worse, pass silently with wrong results) when run in isolation or in a different order, which testthat may do internally. Avoid tests with side effects like writing to disk or modifying global state; if a test genuinely needs temporary files, use withr::local_tempfile() to guarantee cleanup even if the test fails partway through.
testthatformalizes manual output-checking into repeatableexpect_*()assertions grouped insidetest_that()blocks.- Test files live under
tests/testthat/and are namedtest-*.Rto match the source file they cover. expect_error()verifies that invalid input produces the correct error, which matters as much as testing the happy path.expect_equal()allows numeric tolerance whileexpect_identical()requires an exact match of type and attributes.- testthat 3rd edition's
expect_snapshot()records full output to a reference file, ideal for testing print methods and error messages. devtools::test()runs the whole suite;devtools::test_file()runs just one file while iterating on a specific function.- Tests must be independent of execution order and free of lingering side effects like unmanaged temporary files or global state changes.
Practice what you learned
1. Where do test files typically live inside an R package, and what naming convention do they follow?
2. What is the key difference between expect_equal() and expect_identical()?
3. Why is testing that a function throws expect_error() on invalid input important?
4. What does testthat's expect_snapshot() do, introduced in the 3rd edition?
5. Why must tests written with testthat be independent of execution order?
Was this page helpful?
You May Also Like
Writing R Packages
Learn how to structure, document, and build a distributable R package using usethis, roxygen2, and devtools.
R and Machine Learning
Explore how to train, evaluate, and tune machine learning models in R using caret, tidymodels, and core algorithms like random forests and logistic regression.
Statistical Analysis in R
Learn how to compute descriptive statistics, run hypothesis tests, and fit regression models using R's base stats package.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics