R Academy · Lesson

Unit Testing with testthat

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

Lesson 3 of 413 steps

Unit Testing with testthat is a free R Academy lesson on CoddyKit. This is lesson 3 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, and your progress syncs across the web and the CoddyKit app. The R Academy course includes 4 lessons in total.

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
Free to start

Learn R with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
43
Lessons
159

Frequently Asked Questions

Is the “Unit Testing with testthat” lesson free?

Yes — any 3 lessons of the R Academy learning path, including “Unit Testing with testthat”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The R Academy course includes 4 lessons in total.

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, so you can start here or from the beginning and move at your own pace. This is lesson 3 of 4.

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