3  Enriching package functionalities

NoteAims
  • Adding supporting data
  • Documenting how the data was generated
  • Adding examples
  • Adding unit tests
  • Checking the package
Tip

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.

TipReminder

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).

Question

Add these two files to the inst/extdata/ folder of your package.

HintHint

inst/extdata/ may not exist yet — create it first. You can do this from R, or simply from your file browser.

dir.create("inst/extdata", recursive = TRUE, showWarnings = FALSE)
file.copy(
    from = c(
        "______",
        "______"
    ),
    to = "______"
)
dir.create("inst/extdata", recursive = TRUE, showWarnings = FALSE)
file.copy(
    from = c(
        "Share/data/Scc1-vs-input.bw",
        "Share/data/Scc1-peaks.narrowPeak"
    ),
    to = "inst/extdata/"
)
list.files("inst/extdata")

Once the files are in inst/extdata/, you can retrieve their installed path with system.file():

system.file("extdata", "Scc1-vs-input.bw", package = "JacquesTestPackage")

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/.

Question

Create an inst/scripts/make-extdata.R file, and describe there how the two example files were produced.

HintHint

The script is documentation, not something that needs to run end-to-end: a mix of comments and (optionally) code is fine. An example is provided in Share/scripts/make-extdata.R.

dir.create("inst/scripts", recursive = TRUE, showWarnings = FALSE)
file.edit("inst/scripts/make-extdata.R")

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.

Question

Add a runnable @examples section to each of your functions, using the example data you just added.

HintHint

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:

#' @examples
#' 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)
#' l

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:

document()
run_examples()

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:

usethis::use_testthat()

This creates a tests/testthat/ folder and a tests/testthat.R runner.

Question

Create a test file for your functions.

HintHint

usethis::use_test("<name>") creates (and opens) tests/testthat/test-<name>.R. Give it a meaningful name, e.g. the function or feature under test.

usethis::use_test("______")
usethis::use_test("computeCoverage")

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): x equals y;
  • expect_true(x) / expect_false(x): x is TRUE / FALSE;
  • expect_s4_class(x, "GRanges") / expect_s3_class(x, "data.frame"): x has the expected class;
  • expect_named(x, c("a", "b")): x has the expected names;
  • expect_error(f()) / expect_warning(f()): calling f() raises an error / warning.
Question

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.

HintHint

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))
})
Question

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.

HintHint

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", {
    # ... run the pipeline into `df` ...
    expect_equal(nrow(df), ______)
    expect_named(df, c("______", "______", "______", "______"))
    expect_true(all(df$ci_low <= df$______ & df$______ <= df$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.

Tip

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:

devtools::test()

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 with Bioconductor requirements.
devtools::check()
rcmdcheck::rcmdcheck()
BiocCheck::BiocCheck()
Question

Attempt to fix the ERRORs and WARNINGs returned by check() and BiocCheck().

HintHint

Read each message carefully — they usually tell you exactly what to fix. Common early offenders:

  • missing @return / @param tags in the documentation;
  • undeclared dependencies (add them with usethis::use_package("<pkg>"));
  • lines longer than 80 characters (a BiocCheck style requirement);
  • functions used without their pkg:: prefix or an @importFrom tag.

Fix, re-run document(), then re-run the check. Iterate until it is clean.