Measuring Test Coverage in R Packages with covr
The covr package is the standard tool for measuring test code coverage in R. It tracks which lines of your package are executed during tests and helps you identify code that lacks test coverage. This guide covers everything you need to start using covr effectively in your package development workflow.
Installation
You can install covr from CRAN or use the development version from GitHub:
# Stable CRAN version
install.packages("covr")
# Development version from GitHub
devtools::install_github("r-lib/covr")
The package is maintained by the r-lib team and works with any testing framework including testthat, RUnit, or custom test setups. Once installed, you can start measuring coverage immediately from any package directory without additional configuration files.
Calculating package coverage
The main function you’ll use is package_coverage(). Run it from your package root directory. The function automatically discovers your test files, instruments your source code with coverage counters, runs the tests, and reports which lines were executed:
library(covr)
# Calculate coverage for your package tests
cov <- package_coverage()
# View the coverage object
cov
# Coverage: 85.71%
# - R/add.R: 80.00%
# - R/subtract.R: 100.00%
The function returns a coverage object showing which files have what percentage of lines covered by your tests. By default, it runs your package tests located in tests/testthat/.
Coverage types
You can measure different types of coverage depending on what you want to analyze:
# Coverage from tests (default)
cov_tests <- package_coverage(type = "tests")
# Coverage from vignette code
cov_vignettes <- package_coverage(type = "vignettes")
# Coverage from examples in documentation
cov_examples <- package_coverage(type = "examples")
# Coverage from all sources combined
cov_all <- package_coverage(type = "all")
This flexibility lets you check your test suite, your examples, and your vignettes — all independently — to confirm each type of code actually runs without errors.
Understanding coverage reports
Interactive HTML reports
The report() function generates an interactive HTML report that lets you browse through your code and see exactly which lines are covered:
cov <- package_coverage()
report(cov)
# Generates and opens coverage-report.html in your browser
This report shows your source code with green highlighting for covered lines and red for uncovered lines. It’s the most useful view for identifying exactly what needs more tests.
Finding uncovered lines
The zero_coverage() function returns only the uncovered lines as a data frame, filtering out everything that your tests already exercise. This targeted view lets you focus on the gaps rather than scrolling through fully-covered files. The output includes the filename, function name, and the exact line ranges that lack coverage:
cov <- package_coverage()
uncovered <- zero_coverage(cov)
# View uncovered lines
print(uncovered)
# filename functions first_line last_line
# 1 R/add.R <anonymous> 10 15
# 2 R/add.R <anonymous> 20 22
This gives you a focused list of exactly which lines need test coverage. Once you have identified the gaps, the next step is deciding what to test and what to exclude. Not every line of code is worth covering — deprecated functions, debug helpers, and platform-specific code paths often cost more to test than they are worth.
Excluding code from coverage
Sometimes you need to exclude certain code from coverage calculations. For example, deprecated functions, debug code, or platform-specific helpers might not be worth testing in all contexts. covr provides three exclusion mechanisms: a .covrignore file for broad patterns, # nocov comments for fine-grained control, and programmatic exclusions via function arguments.
Using .covrignore file
Create a .covrignore file in your package root to exclude entire directories or specific files from coverage tracking:
# Exclude entire directories
inst/
# Exclude specific files
R/deprecated.R
tests/
Using comments in source code
For more precise control over individual lines or blocks, add # nocov comments directly in your R files. This approach is useful when you want to exclude a single error-handling branch or a debug logging statement without excluding the entire function. The comments work at the granularity of individual expressions:
# Exclude a single line
f1 <- function(x) {
x + 1 # nocov
}
# Exclude a block of code
f2 <- function(x) { # nocov start
message("Debug info")
x + 2
} # nocov end
For compiled code, use // # nocov instead. The comment-based approach works with C, C++, and Fortran source files in the src/ directory as well, using the language-appropriate comment syntax.
Using function arguments
You can also exclude code programmatically using the function_exclusions and line_exclusions arguments to package_coverage(). This method is useful when you want exclusion rules that live in your test or CI configuration rather than embedded in source files:
# Exclude specific functions by pattern
cov <- package_coverage(
function_exclusions = c(
"^\\.helper", # Internal helper functions
"^print\\.", # Print methods
"^format\\." # Format methods
)
)
# Exclude specific line ranges in files
cov <- package_coverage(
line_exclusions = list(
"R/utils.R" = c(1:10, 15),
"R/deprecated.R"
)
)
Measuring coverage for individual functions
For quick checks or non-package code, use function_coverage() to measure coverage of a single function. This is faster than package_coverage() because it does not install or instrument an entire package — it works directly on a function object in your current R session and evaluates a code expression to determine which lines execute:
add <- function(x, y) {
if (!is.numeric(x) || !is.numeric(y)) {
stop("Both arguments must be numeric")
}
x + y
}
# Test without covering the error condition
cov1 <- function_coverage(fun = add, code = add(1, 2))
# Returns: Coverage: 50%
# Test with full coverage
cov2 <- function_coverage(
fun = add,
code = {
add(1, 2) == 3
tryCatch(
add("a", 2),
error = function(e) TRUE
)
}
)
# Returns: Coverage: 100%
This is useful for testing utility functions outside a package context. The function-level coverage API gives you a lightweight way to measure coverage without setting up a full package structure, making it practical for scripts and one-off analyses where you still want to know which code paths your tests exercise.
Common issues and solutions
Cannot run during R CMD check
covr modifies your package code for instrumentation by inserting tracking counters into the source. Running it during R CMD check produces unreliable results because the check process itself alters the execution environment. Run coverage as a separate, dedicated step:
# Don't do this in your package's tests!
# Run coverage separately instead
cov <- package_coverage()
Parallel code
If your package uses parallel processing via the parallel, foreach, or future packages, the worker processes spawned during tests may exit before covr can collect their coverage data. Setting the COVR_FIX_PARALLEL_MCEXIT environment variable instructs covr to instrument the worker processes as well, ensuring coverage from parallel code is captured:
Sys.setenv(COVR_FIX_PARALLEL_MCEXIT = "true")
# Or
options(covr.fix_parallel_mcexit = TRUE)
Compiled code coverage
For C, C++, or FORTRAN code coverage, you need gcov on your system path. The covr package can instrument compiled code in addition to R source files, but this requires the GNU coverage tool and compiler flags that disable optimization. Configure the path to gcov explicitly if your system uses a non-standard installation:
# Set custom gcov path if needed
options(covr.gcov = "/path/to/gcov")
# Disable compiled code coverage if unavailable
options(covr.gcov = "")
Compiled code coverage requires turning off compiler optimization. The covr package handles this automatically when running coverage by adding the appropriate flags (-O0 -fprofile-arcs -ftest-coverage for GCC) to the compilation step. This means coverage runs produce a slower, unoptimized build, which is why they should not be used for performance benchmarks.
Debugging installation issues
If coverage runs fail during the package installation phase, keep the temporary installation directory for inspection. The clean = FALSE argument preserves the instrumented package so you can examine what went wrong:
cov <- package_coverage(
path = ".",
install_path = "/tmp/my_package_lib",
clean = FALSE
)
This keeps the temporary installation so you can inspect what went wrong.
Example workflow
Here’s a typical workflow for improving coverage iteratively. Start by measuring your current baseline, identify the gaps with zero_coverage(), add targeted tests for the uncovered paths, and re-measure to confirm improvement. This feedback loop lets you see your coverage percentage climb as you work:
# 1. Calculate current coverage
cov <- package_coverage()
# 2. Find uncovered lines
uncovered <- zero_coverage(cov)
print(uncovered)
# 3. Add tests to cover those lines
# (edit your test files)
# 4. Re-run coverage
cov <- package_coverage()
cov
# 5. Generate report for detailed review
report(cov)
Interpreting coverage reports
covr::package_coverage() returns a coverage object that shows what percentage of each file’s lines were executed during testing. report() renders it as an HTML page with line-by-line coloring: green for covered lines, red for uncovered. Lines that are not reachable (error branches, defensive checks for impossible inputs) appear uncovered even if the code is correct, coverage does not distinguish between untested code and unreachable code.
Line coverage is a proxy for test quality, not a guarantee of correctness. 80% line coverage means 20% of lines are never executed by any test, those lines may contain untested edge cases or error paths. 100% line coverage does not mean all behavior is tested; two tests can cover the same line with different inputs and only one path is verified.
Coverage percentage is a useful headline metric but a poor optimization target. Chasing 100% coverage by writing tests that merely execute code without making assertions produces high coverage numbers with no corresponding quality improvement. Coverage measures execution, not correctness, a test that calls a function and ignores the result covers the code without verifying anything.
The most useful metric is not the overall percentage but which code paths are uncovered. Use covr::zero_coverage() to see uncovered lines and prioritize testing the most critical paths: error handling, boundary conditions, and complex conditional logic, rather than chasing a coverage percentage. A 95% coverage score that misses all error-handling code is less informative than 80% coverage that exercises the main functionality thoroughly. Review each uncovered section and decide whether to write a test for it or decide it is acceptable to leave uncovered. Error handling code, fallback paths, and edge cases are the most important targets for coverage improvement because they are the code most likely to contain latent bugs.
CI/CD coverage tracking
Codecov, Coveralls, and GitHub Actions all integrate with covr. covr::codecov() uploads a coverage report to Codecov from CI. A coverage badge in README.md shows the current coverage percentage and links to the detailed report. Setting a minimum coverage threshold in CI (e.g., fail if coverage drops below 80%) prevents merging code that reduces test coverage without discussion.
For package testing specifically, coverage over 80% for exported functions is a reasonable target for CRAN packages. The goodpractice package runs covr and reports coverage alongside other package quality checks.
For packages with C/C++ code (Rcpp), covr can measure C-level coverage as well as R-level coverage. package_coverage(type = c('none', 'gcov')) enables gcov-based C coverage. The covr report includes C source files alongside R source files when C coverage is enabled.
What test coverage measures
Code coverage measures what fraction of your code is executed when your tests run. A coverage report shows which lines, branches, and functions are covered, executed at least once during the test suite, and which are not. Low coverage does not mean the code is buggy, but it means the code has not been exercised, so bugs in uncovered code would not be caught.
The covr package generates coverage reports for R packages. It instruments the package code, inserting counters at each expression, then runs the test suite and records which counters were triggered. The output is a coverage percentage and a line-by-line report showing which lines were hit. The report can be viewed in RStudio, rendered as an HTML report, or uploaded to Codecov or Coveralls for integration with pull request workflows.
CI integration
Uploading coverage to Codecov or Coveralls tracks coverage over time and adds coverage feedback to pull requests. A PR that decreases coverage triggers a warning, prompting code reviewers to consider whether the decrease is acceptable. Maintaining coverage above a threshold is a useful team norm for packages that are actively developed by multiple contributors.
The usethis package provides use_github_action(“test-coverage”) to add a standard coverage workflow to a package repository. The workflow installs the package, runs tests with covr, and uploads the report. The Codecov GitHub Action handles authentication and report upload automatically when the repository is configured with the Codecov service.
Summary
The covr package provides comprehensive tools for measuring test coverage in R packages. Key functions include package_coverage() for package-level analysis, report() for interactive HTML reports, and zero_coverage() for finding untested code. The package supports flexible exclusion methods, works with any testing framework, and integrates easily with CI services like GitHub Actions and Codecov.
Remember that 100% coverage isn’t always the goal—focus on covering critical paths and edge cases that matter for your package’s reliability.
See also
- Building R Packages: Learn how to create and structure R packages from scratch
- Debugging in R: Techniques for finding and fixing bugs in your R code
- Testing Shiny Apps with shinytest2: Testing strategies for Shiny applications
- Unit Testing with testthat: Write reliable unit tests for your R code