Write tests for .onLoad in package

Hi,

I'm developing a package in which I set several options using .onLoad, and I want to write tests for the behavior. I believe I would most likely need to do this in a new R subprocess using callr::r(), but I'm not sure exactly how. E.g.

.onLoad Function, located at R/zzz.R

.onLoad <- function(libname, pkgname) {

  op <- options()
  op.new <- list(
      new.opt1 = 5,
      new.opt2 = "hello",
  )

  toset <- !(names(op.new) %in% names(op))
  if (any(toset)) options(op.new[toset])

  invisible()

}

Set-up testing, located at tests/testthat/test-zzz.R

test_that("test .onLoad", {
    callr::r(function() {
      loadNamespace("mypkg")
   # and here I would write checks that the options set with .onLoad() have actually worked
    })

})

However, the attempt to use loadNamespace() (or library()) results in the error that "there is no package called ‘mypkg’". What would be the proper way to reference an in-development package in its own tests like this?

You can probably do this with callr::r(), at least I remember doing similar things in the past. One catch is that you need to load the package in the subprocess differently in R CMD check and in devtools::test() (and testthat::test_local() etc.).

In R CMD check the package is installed and it is on the library path, so you should be able to load it normally.

Otherwise you'd load it with load_all(). Something like this may work, within test_that():

is_chk <- Sys.getenv("_R_CHECK_PACKAGE_NAME_", "") != .packageName
test_data <- if (is_chk) {
  callr::r(function() { library(<yourpackage>); ... })
} else {
  callr::r(function() { pkgload::load_all(); ... })
}

The ... part would return whatever information you need to test that .onLoad() worked properly. (You can't run testthat::expect_*() in the callr subprocess, the testing happens in the main process.

It would probably make sense to skip_on_cran() this test, because it is easy to miss some platform dependent detail that might cause it to fail on CRAN, even if it is OK on your local testing.

1 Like

Thanks for the info @Gabor--this was really useful for me in troubleshooting!

For anyone who looks to do the same, the template code below should be sufficient for the following scenarios:

  1. Running tests interactively with e.g. testthat::test() and testthat::test_active_file()
  2. Running an R CMD check locally using either the bash command or devtools::check()
  3. Running covr::package_coverage() particularly if you're using the GitHub Action for checking and listing Codecov code coverage.
  4. Skipping the tests on CRAN submission
testthat::skip_on_cran()

test_that("test .onLoad", {
  
  # collect options before and after loading the package
  # use different R subprocesses based on loading context
  option_output <- {
    if (testthat:::in_rcmd_check() || testthat:::in_covr()) {

      callr::r(function() {
        preOpts <- options()
        library(ordinalsimr)
        ordinalsimr:::.onLoad()
        postOpts <- options()
        return(list(preOpts = preOpts, postOpts = postOpts))

      })
    } else if (!testthat:::in_rcmd_check()) {

      callr::r(function() {
        preOpts <- options()
        pkgload::load_all()
        ordinalsimr:::.onLoad()
        postOpts <- options()
        return(list(preOpts = preOpts, postOpts = postOpts))

      })

    }
  }
  
  # write tests using the option_output object here
  expect_type(option_output, "list")

  
})

Two tips:

  • Instead of the testthat internal functions, you can use pkgload::is_dev_package() to decide if your package was loaded with load_all(), in which case you would use load_all() in the subprocess as well:

    ❯ pkgload::load_all()
    ℹ Loading nanoparquet.cli
    ❯ pkgload::is_dev_package("nanoparquet.cli")
    [1] TRUE
    ❯ pkgload::is_dev_package("nanoparquet")
    [1] FALSE
    
  • You don't need to call .onLoad() explicitly, library() will call it, and load_all() also call it automatically, that's the whole point of .onLoad().

1 Like

There might be a more eloquent way around the issue, but I had found an explicit call to .onLoad() was needed to get {covr} to detect that the function was actually being tested and for the tests to be counted towards the code coverage.

1 Like