Build reliable, production-grade R code through comprehensive testing strategies from basic assertions to advanced validation frameworks.
Mastering Testing in R
1. The Why and How of Testing
Core Principle: Automated verification that your code behaves as expected under various conditions.
Key Testing Types
Test Type | Purpose | When to Use |
---|---|---|
Unit Tests | Verify individual functions | During development |
Integration Tests | Check component interactions | After major features complete |
Regression Tests | Prevent recurring bugs | After bug fixes |
# Example function to test
calculate_discount <- function(price, discount_rate) {
if (discount_rate < 0 | discount_rate > 1) {
stop("Discount rate must be between 0 and 1")
}
price * (1 - discount_rate)
}
2. testthat Framework
Industry Standard: R's most widely-used testing package (part of tidyverse)
Essential Assertions
test_that("Discount calculation works", {
# Equality
expect_equal(calculate_discount(100, 0.2), 80)
# Error checking
expect_error(calculate_discount(100, -0.1))
# Type checking
expect_type(calculate_discount(100, 0.1), "double")
# Snapshot test
expect_snapshot(calculate_discount(100, 0.3))
})
Test Organization
my_package/ ├── R/ │ └── discounts.R └── tests/ ├── testthat/ │ └── test-discounts.R └── testthat.R
3. Advanced Testing Techniques
Test Fixtures
# tests/testthat/helper-discounts.R
create_test_products <- function() {
list(
normal = list(price = 100, discount = 0.2),
premium = list(price = 200, discount = 0.4)
)
}
# tests/testthat/test-discounts.R
test_that("Discounts apply correctly", {
products <- create_test_products()
expect_equal(
calculate_discount(products$normal$price, products$normal$discount),
80
)
})
Parameterized Testing
test_cases <- tibble::tribble(
~price, ~discount, ~expected,
100, 0.1, 90,
200, 0.25, 150,
50, 0.5, 25
)
purrr::pwalk(test_cases, function(price, discount, expected) {
test_that(glue::glue("Discount {discount*100}% works"), {
expect_equal(calculate_discount(price, discount), expected)
})
})
4. Performance and Benchmarking
Benchmarking with microbenchmark
library(microbenchmark)
microbenchmark(
base = calculate_discount(100, 0.2),
vectorized = Vectorize(calculate_discount)(100, 0.2),
times = 1000
)
Load Testing
test_that("Function handles load", {
prices <- runif(10000, 10, 1000) # 10,000 random prices
discounts <- runif(10000, 0, 0.5)
results <- purrr::map2_dbl(prices, discounts, calculate_discount)
expect_length(results, 10000)
})
5. Continuous Integration
GitHub Actions Setup
# .github/workflows/R-CMD-check.yaml
name: R-CMD-check
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: r-lib/actions/setup-r@v2
- uses: r-lib/actions/setup-pandoc@v2
- name: Install dependencies
run: |
install.packages(c("testthat", "devtools"))
devtools::install_deps(dependencies = TRUE)
- name: Test
run: devtools::test()
- name: Check
run: devtools::check()
6. Specialized Testing Scenarios
Testing Shiny Apps
library(shinytest2)
test_that("Shiny app works", {
app <- AppDriver$new()
app$set_inputs(slider = 50)
app$click("calculate")
expect_equal(app$get_value(output = "result"), 150)
})
Testing R Markdown
test_that("Report renders correctly", {
rmarkdown::render("analysis.Rmd", quiet = TRUE)
expect_true(file.exists("analysis.html"))
})
×