pr_init("add-tests")Testing with testthat - Part 1
Test Infrastructure Setup
First, let’s start a fresh branch for our tests:
Initialize testthat
use_testthat()This creates:
tests/testthat/directory for test filestests/testthat.Rto run tests- Adds testthat to
Suggestsin DESCRIPTION
Create a test file
All new code should have an accompanying test.
Tests for R/{name}.R go in tests/testthat/test-{name}.R.
use_test("cpue")This creates tests/testthat/test-cpue.R matching your R/cpue.R file.
Writing Tests
Basic test structure
test_that("cpue calculates simple ratio correctly", {
expect_equal(cpue(catch = 100, effort = 10), 10)
expect_equal(cpue(catch = 50, effort = 2), 25)
})test_that("cpue handles vectors of data", {
catches <- c(100, 200, 300)
efforts <- c(10, 10, 10)
expected_results <- c(10, 20, 30)
expect_equal(cpue(catches, efforts), expected_results)
})When possible, use a specific expectation which will give an informative failure message, instead of expect_true() or expect_false(). For example, instead of:
expect_true(is.numeric(cpue(100, 10)))Use:
expect_type(cpue(100, 10), "double")Testing optional arguments
test_that("gear_factor standardization scales correctly", {
expect_equal(cpue(catch = 100, effort = 10, gear_factor = 0.5), 5)
expect_equal(
cpue(catch = 100, effort = 10),
cpue(catch = 100, effort = 10, gear_factor = 1)
)
})Testing edge cases
test_that("cpue handles missing data", {
expect_true(is.na(cpue(NA_real_, 10)))
expect_true(is.na(cpue(100, NA_real_)))
})Run your tests
test()pr_push()Test Helpers and Fixtures
Create a test helper file
use_test_helper()Test helpers are loaded before tests run. Create tests/testthat/helper.R:
# tests/testthat/helper.R
# Helper function to generate sample fishing data
generate_fishing_data <- function(n = 10) {
set.seed(67)
data.frame(
catch = runif(n, 10, 500),
effort = runif(n, 1, 20),
gear_factor = runif(n, 1, 5)
)
}Using helpers in tests
test_that("cpue works with generated data", {
data <- generate_fishing_data(n = 5)
result <- cpue(data$catch, data$effort)
expect_equal(
result,
c(34.053, 9.065, 19.239, 135.640, 6.372),
tolerance = 1e-3
)
})Create a setup file
Setup runs once before all tests. Create tests/testthat/setup.R:
# tests/testthat/setup.R
# Set up test fixtures that are expensive to create
# (only created once, shared across all tests)
# Reference dataset for testing
reference_data <- data.frame(
catch = c(100, 200, 300),
effort = c(10, 10, 10),
expected_cpue = c(10, 20, 30)
)Using setup objects in tests
Objects created in setup.R are available to all tests:
test_that("cpue matches reference data", {
result <- cpue(reference_data$catch, reference_data$effort)
expect_equal(result, reference_data$expected_cpue)
})test()Testing messages with expect_message
Let’s add a verbose parameter to cpue so we can test messages. Don’t forget to update the function documentation with the new verbose argument.
Update R/cpue.R – add the parameter and update the roxygen documentation:
#' Calculate Catch Per Unit Effort (CPUE)
#'
#' Calculates CPUE from catch and effort data, with optional gear standardization.
#'
#' @param catch Numeric vector of catch (e.g., kg)
#' @param effort Numeric vector of effort (e.g., hours)
#' @param gear_factor Numeric adjustment for gear standardization (default is 1)
#' @param verbose Logical indicating whether to print processing messages (default is FALSE)
#'
#' @return A numeric vector of CPUE values
#' @export
#'
#' @examples
#' cpue(100, 10)
#' cpue(100, 10, gear_factor = 0.5)
cpue <- function(catch, effort, gear_factor = 1, verbose = FALSE) {
if (verbose) {
message("Processing ", length(catch), " records")
}
raw_cpue <- catch / effort
raw_cpue * gear_factor
}Now test the message:
test_that("cpue provides informative message when verbose", {
expect_message(
cpue(c(100, 200), c(10, 20), verbose = TRUE),
"Processing 2 records"
)
})
test_that("cpue is silent by default", {
expect_no_message(cpue(100, 10))
})Snapshot Testing Introduction
Snapshot testing captures output and compares against a saved reference.
When to use snapshots
- Complex text output (print methods, messages)
- Errors and warnings
- When testing errors and warnings, don’t us
expect_error()orexpect_warning(). Instead, useexpect_snapshot(error = TRUE)for errors andexpect_snapshot()for warnings because these allow the user to review the full text of the output.
- When testing errors and warnings, don’t us
- Plots (with vdiffr package)
Basic snapshot test for errors
test_that("cpue errors when input is not numeric", {
expect_snapshot(
cpue("five", 10),
error = TRUE
)
})First run creates the snapshot
When you first run this test, testthat creates a snapshot file in tests/testthat/_snaps/, and you will get a warning that a new snapshot was added.
test()Snapshot testing for warnings
Let’s use snapshots so we can review the full warning and save ourselves from having to copy and paste the warning text into our test code.
test_that("cpue warns when catch and effort lengths differ", {
expect_snapshot(
cpue(c(100, 200, 300), c(10, 20))
)
expect_no_warning(cpue(100, 10))
})We’ll dive deeper into snapshot testing in Part 2
Run All Tests and Check
test()
check()Your Turn
Add some tests for biomass_index(). Try to make at least one snapshot test.
With R/biomass.R open, run:
use_test()Or
use_test("biomass")Our solutions
Yours may look different. Let’s discuss!
test_that("biomass_index calculates correctly", {
expect_equal(biomass_index(cpue = 10, area_swept = 5), 50)
expect_equal(biomass_index(cpue = 20, area_swept = 2.5), 50)
})
test_that("biomass_index handles vectors", {
cpue_vals <- c(10, 20, 30)
area_vals <- c(5, 5, 5)
expect_equal(biomass_index(cpue_vals, area_vals), c(50, 100, 150))
})
test_that("biomass_index throws error on invalid input", {
expect_snapshot(biomass_index("ten", 5), error = TRUE)
expect_snapshot(biomass_index(10, "five"), error = TRUE)
})Wrap Up
Once everything is passing, push any remaining commits and open the PR for review:
pr_push()Merge the PR on GitHub once CI passes, then clean up your branch:
pr_finish()