Testing R Code with testthat: A Complete Guide
Testing R code with automated unit tests catches regressions before they reach production. The testthat package provides an intuitive framework for writing and running tests, designed specifically for R package development. This guide walks you through setting up testthat, writing your first tests, and integrating testing into your development workflow.
Installing testthat
Install testthat from CRAN or install the latest development version from GitHub:
# From CRAN
install.packages("testthat")
# Development version
install.packages("devtools")
devtools::install_github("r-lib/testthat")
Once installed, testthat needs to be loaded in every session that runs tests. Both CRAN and GitHub sources provide the same API; pick CRAN for stable releases or GitHub if you need the latest features not yet on CRAN:
Load the package with:
library(testthat)
Package structure for testing
With testthat loaded, the next step is understanding where test files live. testthat expects tests inside an R package, following a specific directory layout that separates test infrastructure from the test cases themselves. The standard structure looks like this:
mypackage/
├── R/
│ ├── function1.R
│ └── function2.R
├── tests/
│ ├── testthat.R
│ └── testthat/
│ ├── test-function1.R
│ └── test-function2.R
└── DESCRIPTION
The tests/testthat.R file is the entry point that initializes the testing infrastructure. It loads testthat, attaches your package, and triggers all test files under tests/testthat/. R CMD check and CI pipelines run this file automatically to execute your entire test suite:
# tests/testthat.R
library(testthat)
library(mypackage)
test_check("mypackage")
All test files go in tests/testthat/ and should start with test-. The naming convention helps testthat discover all test files automatically during a test run.
Understanding expectations
Expectations are the building blocks of tests. Each expectation asserts a single property about your code’s output or behavior: that a value equals an expected result, that a function throws an error, or that an object has the right class. The basic syntax wraps these assertions in a consistent pattern:
expect_* (object, expected)
The most common expectation checks whether a computed result equals an expected value. testthat provides two equality checks that differ in strictness: expect_equal() tolerates small floating-point differences, while expect_identical() requires exact type and value matches.
Equality expectations
expect_equal(x, y), tests for near equality (good for floating-point numbers)expect_identical(x, y), tests for exact equality
expect_equal(2 + 2, 4)
# [1] TRUE
expect_identical(c(1L, 2L, 3L), 1:3)
# [1] TRUE
Error and warning expectations
Equality expectations verify that correct inputs produce correct outputs. But well-tested code also handles invalid inputs gracefully: catching errors when inappropriate arguments are passed, or detecting warnings that signal unexpected conditions. testthat provides dedicated expectations for each of these scenarios.
expect_error(code), checks if code throws an errorexpect_warning(code), checks if code produces a warningexpect_silent(code), checks if code runs without errors or warnings
expect_error(log("not a number"))
expect_error(stop("custom error message"), "custom error message")
expect_warning(log(-1))
expect_silent(2 + 2)
Type expectations
Beyond values and errors, tests should confirm that the objects your functions return have the correct types and classes. A function documented to return a data frame should actually return a data frame, and type expectations provide the guardrails for this contract:
expect_type(x, type), checks base R typeexpect_s3_class(x, "classname"), checks S3 classexpect_s4_class(x, "classname"), checks S4 class
expect_type(c(1, 2, 3), "double")
expect_type(1:3, "integer")
expect_s3_class(data.frame(x = 1:3), "data.frame")
Writing your first test
Individual expectations verify single properties, but a complete test groups related expectations together to describe one behavior. The test_that() function takes a description string and a block of expectations, creating a named unit that testthat reports as passed or failed. Here is a function and its corresponding test file:
# R function to test
get_sum <- function(x) {
if (!is.numeric(x)) {
stop("Input must be numeric")
}
sum(x)
}
# Test file: tests/testthat/test-get_sum.R
test_that("get_sum calculates the sum correctly", {
expect_equal(get_sum(c(1, 2, 3)), 6)
expect_equal(get_sum(c(0, 0)), 0)
expect_equal(get_sum(-5), -5)
})
test_that("get_sum handles invalid inputs", {
expect_error(get_sum("not numeric"))
expect_error(get_sum(NULL))
})
Each test_that() block describes the behavior under test; the description becomes part of the failure message when something breaks, so it should name the expected behavior, not the implementation. Once your test file is written, you need a way to execute it during development and in CI pipelines.
Running your tests
Run tests during development using one of these methods:
# From your package root
devtools::test()
# Or using testthat directly
testthat::test_local()
# Run a specific test file
testthat::test_file("tests/testthat/test-get_sum.R")
When all tests pass, testthat prints a summary table showing counts of passed, failed, and skipped tests along with a check mark. The concise format makes it easy to scan: a single line confirms everything is green, and any failures appear as red X marks with the full error context below.
✔ | OK FAILED SKIPPED
1 | 1
[1] TRUE
When a test fails, testthat shows you exactly which expectation failed and what went wrong, displaying the expected and actual values side by side. This targeted output eliminates the need to add print debugging inside tests.
Testing conditions
Beyond value comparisons, testthat lets you assert that functions produce specific warnings, errors, or messages: the side channels of R’s condition system. Use expect_condition(), expect_warning(), and expect_message() to verify your code signals the right conditions at the right time:
test_that("log produces warning for negative input", {
expect_warning(log(-1), "NaNs produced")
})
test_that("function produces message", {
expect_message(hello(), "Hello!")
})
Snapshot testing
Snapshot tests capture the full output of a function call and compare it against a stored reference file. When you run expect_snapshot() for the first time, testthat saves the output to a .txt snapshot file in tests/testthat/_snaps/. On subsequent runs, it compares the current output to the saved snapshot and reports any differences. This is especially useful for testing formatted output from print() methods, error messages, and rendered reports where manually constructing the expected string would be tedious.
When you intentionally change behavior that affects snapshot output, run testthat::snapshot_review() to interactively review and accept the new output. Snapshot files should be committed to version control so they are available in CI runs and for other developers on the team.
Test fixtures and isolation
Tests that require shared setup, such as database connections, file paths, or large objects, benefit from fixtures that are automatically cleaned up after each test. The testthat package provides local_*() helpers for this: withr::local_options() sets R options for the duration of a single test and restores them afterward; withr::local_envvar() sets environment variables; and withr::local_tempdir() creates a temporary directory that is removed when the test completes. These helpers prevent test pollution where changes made in one test leak into the next.
For mocking external dependencies, testthat::local_mocked_bindings() replaces a function within the scope of a test block and restores the original after the test finishes. This built-in mocking facility eliminates the need for external mocking packages in most common cases. When a function calls an external API or reads from a file, mock it to return a known response, making tests fast and deterministic without network or filesystem dependencies.
Code coverage and CI integration
Measuring code coverage identifies untested code paths. Install the covr package and run covr::package_coverage() to see which lines your tests execute and which they miss. The covr::report() function opens an interactive HTML report with green lines for covered code and red lines for uncovered code. Aim for at least 80% coverage as a reasonable baseline — below 60% indicates significant gaps in the test suite.
For continuous integration, usethis::use_github_actions() sets up a GitHub Actions workflow that installs dependencies, runs R CMD check, and reports results on every push. Add a coverage step with covr::codecov() to track coverage trends over time. The default workflow catches regressions before they reach users by running the full test suite in a clean environment on each commit.
See also
- Building R Packages — setting up package structure
- Writing R Functions — best practices for function design
- R Debugging — techniques for finding and fixing bugs
Start small with a few tests for your most important functions. As your package grows, testthat scales with you, making it easy to maintain confidence in your code.