3 Enriching package functionalities
- Adding supporting data
- Documenting how the data was generated
- Adding examples
- Adding unit tests
- Checking the package
At any time, if you are lost or do not understand how functions in the proposed solution work, type ?<function> in the R console and a help menu will appear.
You can also check the help tab in the corresponding quadrant.
We aim to create a package which can plot the aggregated coverage of a genomic track over a set of GRanges of interest (at a fixed width).
By now, your package should contain the four functions (importFiles(), filterGRanges(), computeCoverage(), plotCoverage()) prototyped in Day 1 and documented in Day 2.
3.1 Add supporting data to your package
To let your users (and yourself!) run the examples and tests, a package should ship small example datasets. Raw data files live in inst/extdata/.
You can use the toy dataset files provided in Share/data/ (Scc1-vs-input.bw and Scc1-peaks.narrowPeak).
Add these two files to the inst/extdata/ folder of your package.
inst/extdata/ may not exist yet — create it first. You can do this from R, or simply from your file browser.
Once the files are in inst/extdata/, you can retrieve their installed path with system.file():
Bioconductor requires that you document how each raw data file was generated or obtained. This is done, less formally, in a .R script placed in inst/scripts/.
Create an inst/scripts/make-extdata.R file, and describe there how the two example files were produced.
3.2 Add examples to your function files
Examples illustrate how your functions work. They are integrated in the manual pages compiled with document(), and are often the first thing a new user reads.
Add a runnable @examples section to each of your functions, using the example data you just added.
Reference the installed data with system.file() so that the examples work on any machine where the package is installed. A roxygen @examples block looks like this:
Fully worked-out function files, with @examples sections added, are available in Share/functions/. For instance, importFiles() chains into the whole pipeline:
#' @examples
#' bw_file <- system.file("extdata", "Scc1-vs-input.bw", package = "JacquesTestPackage")
#' bed_file <- system.file("extdata", "Scc1-peaks.narrowPeak", package = "JacquesTestPackage")
#' importFiles(bw_file, bed_file, width = 2000) |>
#' filterGRanges() |>
#' computeCoverage() |>
#' plotCoverage()Once you have added examples and re-run document(), you can check that they all run without error:
3.3 Add unit tests to your package
Unit tests are the best way to make sure your package keeps working as intended as you keep editing it. Each test runs a bit of R code and compares the result to an expectation. We use the testthat framework.
To start using unit tests, declare it once:
This creates a tests/testthat/ folder and a tests/testthat.R runner.
A test file is a series of test_that("description", { ... }) blocks. Inside each block, you state expectations with the expect_*() family:
-
expect_equal(x, y)/expect_identical(x, y):xequalsy; -
expect_true(x)/expect_false(x):xisTRUE/FALSE; -
expect_s4_class(x, "GRanges")/expect_s3_class(x, "data.frame"):xhas the expected class; -
expect_named(x, c("a", "b")):xhas the expected names; -
expect_error(f())/expect_warning(f()): callingf()raises an error / warning.
Write a test that checks that importFiles() returns a list with a coverage and a features element, and that every feature has been resized to the requested width.
Load the example data with system.file(), call importFiles(), then state what you know must be true about the result.
test_that("importFiles() resizes features to the requested width", {
bw_file <- system.file("extdata", "______", package = "______")
bed_file <- system.file("extdata", "______", package = "______")
l <- ______(bw_file, bed_file, width = 2000)
expect_named(l, c("______", "______"))
expect_true(all(______(l$features) == ______))
})test_that("importFiles() resizes features to the requested width", {
bw_file <- system.file("extdata", "Scc1-vs-input.bw", package = "JacquesTestPackage")
bed_file <- system.file("extdata", "Scc1-peaks.narrowPeak", package = "JacquesTestPackage")
l <- importFiles(bw_file, bed_file, width = 2000)
expect_type(l, "list")
expect_named(l, c("coverage", "features"))
expect_s4_class(l$features, "GRanges")
expect_true(all(GenomicRanges::width(l$features) == 2000))
})Write a test that checks the shape and consistency of the data.frame returned by computeCoverage(): one row per position, the expected column names, and a confidence interval that always brackets the mean.
Run the full pipeline, then think about the invariants that must hold no matter the data: nrow(df) == width, the columns are distance/mean/ci_low/ci_high, and for every position ci_low <= mean <= ci_high.
test_that("computeCoverage() returns a well-formed data.frame", {
bw_file <- system.file("extdata", "Scc1-vs-input.bw", package = "JacquesTestPackage")
bed_file <- system.file("extdata", "Scc1-peaks.narrowPeak", package = "JacquesTestPackage")
width <- 2000
df <- importFiles(bw_file, bed_file, width = width) |>
filterGRanges() |>
computeCoverage()
expect_s3_class(df, "data.frame")
expect_equal(nrow(df), width)
expect_named(df, c("distance", "mean", "ci_low", "ci_high"))
expect_equal(df$distance[1], -width / 2)
expect_true(all(df$ci_low <= df$mean))
expect_true(all(df$mean <= df$ci_high))
})A complete, working example test file is available in Share/tests/test-computeCoverage.R.
Good tests often check edge cases and error handling, not just the happy path. For instance: does computeCoverage() error out cleanly if the features do not all share the same width? You could test that with expect_error(...).
Once your tests are written, run the whole suite:
3.4 Check your package
Several check functions are available. They each check your package in a specific way:
-
devtools::check()verifies the basic structure of the package, the documentation, the examples and the unit tests; -
rcmdcheck::rcmdcheck()does somewhat the same job but builds the package first; -
BiocCheck::BiocCheck()checks the compliance of the package withBioconductorrequirements.
Attempt to fix the ERRORs and WARNINGs returned by check() and BiocCheck().
Read each message carefully — they usually tell you exactly what to fix. Common early offenders:
- missing
@return/@paramtags in the documentation; - undeclared dependencies (add them with
usethis::use_package("<pkg>")); - lines longer than 80 characters (a
BiocCheckstyle requirement); - functions used without their
pkg::prefix or an@importFromtag.
Fix, re-run document(), then re-run the check. Iterate until it is clean.