Historical Context & Motivation
Software testing has deep roots in computer science, stretching back to the early days of structured programming when developers first recognized that verifying correctness was as important as writing the code itself. In the R ecosystem, testing was historically an informal process—analysts would run snippets in the console, visually inspect output, and trust that things looked right. This manual approach, while workable for small scripts, became untenable as R packages grew in complexity and community adoption. The need for a formal, automated testing framework for R became apparent as the language transitioned from a niche statistical tool into a full-featured programming environment used in industry and research alike.
The testthat package, created by Hadley Wickham, drew inspiration from established testing frameworks in other languages—particularly xUnit frameworks like JUnit (Java) and RSpec (Ruby). These frameworks popularized the idea that tests should be declarative, readable, and organized into logical groups. Wickham adapted these principles for R's unique functional programming style, producing a testing library that felt natural to R developers while adhering to software engineering best practices.
test_that() and expect_equal() functions.The central question testthat addresses is deceptively simple: how do you ensure that your R code continues to work correctly as it evolves? Without automated tests, every change to a function introduces the risk of silently breaking existing behavior—a phenomenon known as regression. By encoding expected behavior in executable tests, testthat transforms the error-prone process of manual verification into a repeatable, automated safety net.
Core Principles & Definitions
At its core, testthat is organized around a three-level hierarchy that mirrors how developers think about correctness. You write individual expectations—atomic assertions about what a function should return—and group them into tests that describe a single behavior. Tests, in turn, live inside test files that correspond to specific modules or source files. This layered structure keeps your test suite organized even as your codebase grows.
test_that()
test_that() call represents a single, self-contained test case.expect_equal()
all.equal() under the hood, making it suitable for floating-point comparisons.expect_identical()
expect_equal() that checks exact identity using R's identical(). No tolerance is applied—types, attributes, and values must match precisely.Test File Convention
tests/testthat/ directory and must be named with the prefix test- (e.g., test-math_utils.R). The test runner auto-discovers and executes all matching files.Descriptive Strings
test_that() is a plain-English description of the expected behavior, e.g., "addition handles negative numbers". These strings serve as documentation and appear in failure reports.test_that() as a contract between you and your code. If you were hiring a contractor to build a house, you wouldn't just ask them to "make it nice"—you'd specify that the foundation must support a certain load, the wiring must pass inspection, and the plumbing must not leak. Each test_that() block is one of those specific, verifiable requirements. The expect_equal() calls inside it are the measurements you take to confirm the requirement is met.Visual Explanation — The testthat Hierarchy
The diagram above illustrates how testthat structures your tests into a clean, navigable hierarchy. At the top level, a test file like test-math_utils.R is automatically discovered by the test runner. Inside that file, each test_that() block focuses on a specific behavior—such as verifying that an add() function correctly sums two numbers, or that it gracefully handles NA inputs. Within each test case, individual expect_equal() calls check concrete input-output pairs. When a test fails, the report pinpoints the exact file, line number, test description, and the discrepancy between expected and actual values, making debugging straightforward.
How testthat Works Under the Hood
Building on the test_that() and expect_equal() functions you just met, this section looks at how those functions decide whether a test passes—background that will make the worked example later in this lesson easier to follow. At the core, expect_equal() delegates to R's built-in all.equal() function, which performs a comparison that accounts for floating-point imprecision. This is critical because computers represent real numbers in IEEE 754 double-precision format, where operations like 0.1 + 0.2 do not yield exactly 0.3 but rather 0.30000000000000004. The tolerance-based comparison in expect_equal() handles this gracefully.
tolerance ≈ 1.5 × 10⁻⁸ (the square root of .Machine$double.eps). The comparison is relative for nonzero expected values and absolute when the expected value is zero.The test_that() Execution Model
Each test_that() block executes its code in an isolated environment. This means that variables defined inside one test_that() block are not visible to another, preventing test pollution—a common source of flaky tests. If an expectation fails, testthat records the failure but continues executing subsequent test_that() blocks (though remaining expectations within the same block are skipped in third-edition testthat). This fail-and-continue model ensures you receive a comprehensive report of all broken tests in a single run.
test_that() block follows this lifecycle. Setup and teardown can be managed with withr::local_*() or setup()/teardown() helpers to ensure test isolation.expect_equal() when comparing numeric values or when minor floating-point differences are acceptable. Use expect_identical() when you need exact matches—especially for character strings, logical values, or when type matters (e.g., integer vs. double). As a rule of thumb: default to expect_equal() and reach for expect_identical() only when precision matters.Detailed Breakdown — The expect_* Family
With the internal comparison logic clear, we can now survey the rest of the expectation toolkit. While expect_equal() is the most commonly used expectation, testthat provides a rich family of expect_*() functions for different assertion scenarios. Understanding when to use each function is essential to writing expressive, maintainable tests. The table below catalogs the most frequently used expectations, grouped by category, along with their typical use cases.
| Function | Checks | Example |
|---|---|---|
expect_equal(x, y) | Near-equality using tolerance | expect_equal(sum(1:3), 6) |
expect_identical(x, y) | Exact identity (type + value) | expect_identical("a", "a") |
expect_true(x) | Value is TRUE | expect_true(is.numeric(42)) |
expect_false(x) | Value is FALSE | expect_false(is.na(5)) |
expect_error(expr) | Expression throws an error | expect_error(log("x")) |
expect_warning(expr) | Expression raises a warning | expect_warning(log(-1)) |
expect_type(x, type) | Checks typeof(x) | expect_type(1L, "integer") |
expect_length(x, n) | Vector has length n | expect_length(1:5, 5) |
expect_*() function. Start at the top by identifying what aspect of your function's behavior you are testing—return value, side effect, or structural property—then follow the branches.The flowchart above provides a systematic approach to selecting the right expectation function. In practice, the majority of your tests will use expect_equal() for verifying computed results. When you need to assert that your function raises an error on invalid input—a critical aspect of defensive programming—expect_error() accepts an optional regular expression that matches against the error message, allowing you to confirm not just that an error occurred, but that it was the right error.
Worked Example — Testing a Temperature Converter
Having surveyed the expect_* family and the decision flowchart for choosing among them, we can now apply those ideas in a complete, realistic example. Suppose we are developing a utility function celsius_to_fahrenheit() that converts a temperature from Celsius to Fahrenheit using the formula F = C × 9/5 + 32. We want to verify correct output for typical values, edge cases like absolute zero, and invalid inputs.
R/conversions.R:
celsius_to_fahrenheit <- function(celsius) {
if (!is.numeric(celsius)) stop("Input must be numeric")
celsius * 9 / 5 + 32
}tests/testthat/test-conversions.R. The test- prefix is mandatory for auto-discovery. At the top, load testthat with library(testthat) (if not already loaded by the test runner).test_that("celsius_to_fahrenheit converts standard values correctly", {
expect_equal(celsius_to_fahrenheit(0), 32)
expect_equal(celsius_to_fahrenheit(100), 212)
expect_equal(celsius_to_fahrenheit(-40), -40)
})
We include three expectations inside one test_that() block because they all test the same behavior: correct conversion of normal inputs. The value −40 is a well-known fixed point where Celsius and Fahrenheit coincide.test_that("celsius_to_fahrenheit handles absolute zero", {
expect_equal(celsius_to_fahrenheit(-273.15), -459.67)
})
Absolute zero (−273.15°C) should map to −459.67°F. Because we are comparing floating-point numbers, expect_equal()'s built-in tolerance ensures this passes even if there is a tiny rounding discrepancy in the last decimal place.test_that("celsius_to_fahrenheit rejects non-numeric input", {
expect_error(celsius_to_fahrenheit("hot"), "Input must be numeric")
expect_error(celsius_to_fahrenheit(TRUE), "Input must be numeric")
})
Both calls should trigger the same validation error. The character string "hot" is an obvious non-numeric value, but TRUE is worth testing too: although logical values behave like 0/1 in arithmetic, is.numeric(TRUE) returns FALSE because logicals are a distinct type from numerics in R. The second argument to expect_error() is a regular expression matched against the error message, ensuring we are catching the intended error rather than some other unexpected failure.devtools::test() or testthat::test_dir("tests/testthat"). The output will show a summary like:
══ Results ══════════════════════
[ PASS 6 | FAIL 0 | WARN 0 | SKIP 0 ]
🎉 All tests passed!Strengths, Limitations & Alternatives
No testing framework is a silver bullet, and understanding where testthat excels—and where it falls short—will help you make informed decisions about your testing strategy. The table below contrasts key strengths and limitations of the framework, particularly as it applies to the kind of data-centric, functional code common in R.
| Strengths | Limitations |
|---|---|
| Expressive, readable test syntax that doubles as documentation for expected behavior. | Primarily designed for package development; using it in standalone scripts requires extra setup. |
| Rich set of expect_* functions covering values, types, errors, warnings, and output. | Does not natively support mocking; requires the companion package mockery or mockr. |
| Tight integration with devtools and RStudio, enabling one-click test execution. | Testing Shiny apps and database interactions requires specialized packages (shinytest2, dbtest). |
| Snapshot testing (3rd edition) captures complex output like plots and data frames. | Tests run sequentially by default; parallel execution is available but may require careful isolation. |
| Excellent error messages that identify the exact file, line, and expected vs. actual values. | Learning curve for test organization conventions (file naming, directory structure) can trip up beginners. |
Connection to Advanced Testing Practices
The test_that() and expect_equal() functions introduced in this lesson are the foundation upon which more sophisticated testing strategies are built. As your R projects grow—whether you are developing a statistical package, a Shiny application, or a production data pipeline—you will encounter scenarios that demand more advanced techniques. The table below maps introductory concepts to their advanced counterparts, giving you a roadmap for further study.
| Introductory Concept | Advanced Extension | When You Need It |
|---|---|---|
expect_equal() | Snapshot testing with expect_snapshot() | When output is complex (plots, printed tables, long text) and hard to express as a simple equality check. |
| Single test file | Test fixtures and shared setup.R / helper.R files | When multiple test files need shared data, database connections, or utility functions. |
| Manual test execution | CI/CD integration (GitHub Actions, Jenkins) | When you want tests to run automatically on every commit or pull request. |
| Testing pure functions | Mocking with mockery::stub() | When functions depend on external services (APIs, databases, file systems) that you want to isolate. |
| Basic pass/fail | Code coverage analysis with covr | When you want to measure what percentage of your source code is exercised by your test suite. |
A particularly important next step is test-driven development (TDD), a discipline in which you write failing tests before implementing the corresponding functionality. The TDD cycle—Red (write a failing test), Green (write minimal code to pass), Refactor (improve code while tests stay green)—leverages testthat as its engine. Mastering the basics of test_that() and expect_equal() is the prerequisite for practicing TDD effectively in R.
Practice Problems
test_that() block that tests a function square <- function(x) x^2. Include expectations for square(3), square(-4), and square(0).expect_equal() and expect_identical(). Give a specific scenario where using expect_identical() on a numeric comparison would cause a test to fail even though expect_equal() would pass.safe_divide <- function(a, b) { if (b == 0) stop("Division by zero"); a / b }. Write a test file that includes: (1) a test case verifying correct division for normal inputs, (2) a test case verifying that dividing by zero raises the correct error message, and (3) a test case verifying that the function handles negative numbers.z_score <- function(x) (x - mean(x)) / sd(x) standardizes a numeric vector. Write tests that verify: the output has mean ≈ 0 and standard deviation ≈ 1 for a non-trivial input vector, the function returns NaN for a constant vector (since sd = 0), and the output length matches the input length.fetch_data(url) that makes an HTTP request and returns a data frame. Explain why testing this function with basic test_that() and expect_equal() is problematic. Propose a testing strategy that uses the testthat fundamentals from this lesson while addressing the challenges of external dependencies. Discuss how test isolation, reproducibility, and CI/CD considerations influence your approach.Lesson Summary
The testthat package provides R with a modern, expressive unit testing framework inspired by xUnit and BDD traditions. Its two most fundamental functions—test_that() and expect_equal()—form the backbone of every test suite. test_that() defines an isolated test case with a human-readable description, while expect_equal() asserts that a computed value matches an expected value within a floating-point tolerance of approximately 1.5 × 10⁻⁸. For exact comparisons, expect_identical() offers a strict alternative.
Tests are organized into test files with the test- prefix, stored in tests/testthat/, and executed via devtools::test(). The broader expect_* family—including expect_error(), expect_true(), expect_type(), and expect_length()—covers side effects, types, and structural properties. Mastering these fundamentals prepares you for advanced practices such as snapshot testing, mocking, code coverage analysis, and test-driven development.