0Pricing
R Academy · Lesson

Unit Testing with testthat

Write test_that() blocks, use expectations, and run tests with devtools::test().

Unit Testing with testthat is a free R Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Unit Testing?

Unit tests automatically verify that individual functions behave correctly. They catch regressions when you change code, serve as executable documentation, and give you confidence to refactor safely. The testthat package is the standard testing framework for R packages.

Setting Up testthat

usethis::use_testthat() adds testthat to Suggests in DESCRIPTION, creates tests/testthat/, and creates the runner script tests/testthat.R. Run it once when initializing a new package.

# usethis::use_testthat()
#
# Creates:
# tests/
#   testthat.R                  <- runner (do not edit)
#   testthat/
#     (empty — write test files here)
#
# Updates DESCRIPTION:
# Suggests: testthat (>= 3.0.0)
# Config/testthat/edition: 3

Creating a Test File

usethis::use_test('add') creates tests/testthat/test-add.R. By convention, test files are named test-{function_name}.R. Each file groups tests for one function or feature.

# usethis::use_test('add')  # creates tests/testthat/test-add.R
#
# Content of test-add.R:
# test_that('add() returns correct sum', {
#   expect_equal(add(1, 2), 3)
#   expect_equal(add(-1, 1), 0)
#   expect_equal(add(0.1, 0.2), 0.3, tolerance = 1e-7)
# })

test_that() Structure

test_that('description', { ... }) groups related expectations. The description string should complete the sentence 'test that ...' and be specific enough to be useful in failure messages.

# Good test_that descriptions:
# test_that('add() handles negative numbers', { ... })
# test_that('add() recycles length-1 vectors', { ... })
# test_that('add() returns NA when input contains NA', { ... })
#
# Bad (too vague):
# test_that('it works', { ... })
# test_that('test1', { ... })

expect_equal() and expect_identical()

expect_equal(actual, expected) tests with a numeric tolerance for floating point. expect_identical(actual, expected) requires exact equality including type. For most cases, expect_equal() is preferable.

# test_that('add() adds correctly', {
#   expect_equal(add(1, 2), 3)            # numeric equality
#   expect_equal(add(0.1, 0.2), 0.3)     # tolerance handles floating point
#   expect_identical(add(1L, 2L), 3L)    # exact type match: integer
#   expect_identical(add(1.0, 2.0), 3.0) # exact type match: double
# })

expect_error() and expect_warning()

Test that functions produce the correct errors and warnings. Pass a regex pattern to match the error message — this ensures the right error is thrown, not just any error.

# test_that('add() validates input types', {
#   expect_error(
#     add('a', 2),
#     regexp = 'numeric'   # message must contain 'numeric'
#   )
#   expect_error(
#     add(NULL, 1),
#     regexp = 'numeric'
#   )
# })
#
# test_that('sqrt() warns on negative input', {
#   expect_warning(sqrt(-1))
# })

expect_true() and expect_false()

expect_true(expr) and expect_false(expr) test logical conditions. Use them when testing predicates or conditions that return a single logical value.

# test_that('is_positive() returns correct logical', {
#   expect_true(is_positive(5))
#   expect_true(is_positive(0.001))
#   expect_false(is_positive(0))
#   expect_false(is_positive(-3))
# })
#
# # Also useful for vector tests:
# test_that('add() result has correct length', {
#   result <- add(c(1,2,3), c(4,5,6))
#   expect_true(length(result) == 3)
# })

More Expectation Functions

testthat provides many expectation functions for different scenarios:

  • expect_length(x, n) — check vector length
  • expect_type(x, 'double') — check base type
  • expect_s3_class(x, 'data.frame') — check S3 class
  • expect_null(x) — check for NULL
  • expect_match(string, regexp) — check string pattern
# test_that('add() output has correct type and length', {
#   result <- add(c(1.0, 2.0), c(3.0, 4.0))
#   expect_type(result, 'double')
#   expect_length(result, 2)
# })
#
# test_that('summary_stats() returns a data frame', {
#   result <- summary_stats(rnorm(100))
#   expect_s3_class(result, 'data.frame')
# })

Running Tests with devtools::test()

devtools::test() (Ctrl+Shift+T) runs all test files and displays a summary of passes, failures, and warnings. Individual test failures show the expectation that failed and the actual vs expected values.

# devtools::test()
#
# Example output:
# == Testing mypackage ====================================
# v | OK F W S | Context
# v |  3       | add [0.1s]
# v |  4       | subtract [0.1s]
# x |  2 1     | multiply [0.2s]
# -- Failure (test-multiply.R:5): multiply() handles zero
# multiply(5, 0) not equal to 0.
# Actual:   5
# Expected: 0
# ==========================================================
# [ FAIL 1 | WARN 0 | SKIP 0 | PASS 9 ]

Test Coverage with covr

covr::package_coverage() measures what percentage of your package's lines are executed by tests. covr::report() opens an HTML report showing covered (green) and uncovered (red) lines. Aim for at least 80% coverage.

# library(covr)
# cov <- package_coverage()
# print(cov)
#
# Example output:
# mypackage Coverage: 87.50%
# R/add.R:      100.00%
# R/subtract.R: 100.00%
# R/utils.R:     62.50%  <- needs more tests!
#
# covr::report()  # interactive HTML report
# covr::zero_coverage(cov)  # list uncovered lines

Testing Edge Cases

Good tests cover not just the happy path but also edge cases:

  • Empty inputs: numeric(0), character(0)
  • NA inputs: does the function propagate or handle NA?
  • Length-1 vs length-n inputs
  • Boundary values: 0, negative numbers, very large values
  • Incorrect types: what happens when the user passes a string to a numeric function?
# test_that('add() handles edge cases', {
#   expect_equal(add(numeric(0), numeric(0)), numeric(0))  # empty
#   expect_true(is.na(add(NA, 1)))                        # NA propagation
#   expect_equal(add(1, c(1,2,3)), c(2,3,4))              # recycling
#   expect_equal(add(.Machine$integer.max, 0L),           # boundary
#               .Machine$integer.max)
# })

Quick Check: expect_error()

You call expect_error(my_fn('bad'), regexp = 'invalid input'). What does this test verify?

Unit Testing Recap

Testing an R package with testthat:

  • usethis::use_testthat() — set up the test infrastructure once
  • usethis::use_test('fn') — create tests/testthat/test-fn.R
  • test_that('description', {...}) — group related expectations
  • expect_equal(), expect_error(), expect_warning(), expect_true(), expect_false() — core expectation functions
  • devtools::test() — run all tests (Ctrl+Shift+T)
  • covr::package_coverage() — measure test coverage

Frequently asked questions

Is the “Unit Testing with testthat” lesson free?

Yes — the full text of “Unit Testing with testthat” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.

What will I learn in “Unit Testing with testthat”?

Write test_that() blocks, use expectations, and run tests with devtools::test(). You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Unit Testing with testthat” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Package Structure with usethis and devtools
  2. Documenting Functions with roxygen2
  3. Unit Testing with testthat
  4. CRAN Submission and Package Maintenance
← Back to R Academy