100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Testing R Code with testthat

Learn how to write, organize, and run automated unit tests for R functions and packages using the testthat framework.

Practical RIntermediate9 min readJul 10, 2026
Analogies

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.

r
# 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.

  • testthat formalizes manual output-checking into repeatable expect_*() assertions grouped inside test_that() blocks.
  • Test files live under tests/testthat/ and are named test-*.R to 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 while expect_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

Was this page helpful?

Topics covered

#Programming#RProgrammingStudyNotes#TestingRCodeWithTestthat#Testing#Code#Testthat#Automated#StudyNotes#SkillVeris#ExamPrep