Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write unit tests for suggested packages?

Tags:

r

testthat

Packages in R can have different types of dependencies on other packages. Some of these types indicate hard requirements, i.e. Depends, Imports and LinkingTo.

However, there is a second category that indicate a softer dependency, i.e. Suggests and Enhances. In both these cases, the package provides additional functionality if the suggested / enhanced package is available.

Here is a concrete example: The package checkpoint imports knitr because knitr helps checkpoint to parse rmarkdown files.

But now I am considering changing knitr to a Suggests dependency, i.e. only provide this functionality if knitr is actually installed.

For proper unit testing, this means I have to test both scenarios:

  1. If knitr is available, then do stuff.
  2. If knitr is not available, then throw a warning and do nothing.

The actual R code is simple:

if(require(knitr)) {
  do_stuff()
} else {
  message("blah")
}

Question

But how can I set up unit tests for both scenarios?

The way I see it, the simple fact of checking for require(knitr) will load the knitr package if it is available in the local library.

So, to test for case 1, I have to install knitr locally, meaning I can't test for case 2.

Is there a way of configuring testthat (or any other unit testing framework) for this use case?

like image 487
Andrie Avatar asked Jan 12 '15 17:01

Andrie


1 Answers

tl;dr

To test the branch followed when use require(knitr) fails, use trace() to temporarily modify require() so that it won't find knitr, even if it is present on .libPaths(). Specifically, in the body of require(), reset the value of lib.loc= to point to R.home() -- an existing directory that does not contain a knitr package.

This seems to work just as well in a package as it will in an interactive session in which you run the following:

find.package("knitr")

trace("require", quote(lib.loc <- R.home()), at=1)
isTRUE(suppressMessages(suppressWarnings(require(knitr))))

untrace("require")
isTRUE(suppressMessages(suppressWarnings(require(knitr))))

As I understand this, you have a function with two branches, one to be performed in R sessions for which require(knitr) succeeds, and the other to be performed in sessions where it fails. You are then wanting to test this function "both ways" from a single R instance in which knitr actually is on .libPaths().

So basically you are needing some way to temporarily blind the call require(knitr) to the actual presence of knitr. Completely and temporarily resetting the value returned by .libPaths() looked promising, but doesn't seem to be possible.

Another promising avenue is to somehow reset the default value of lib.loc in calls to require() from NULL (which means "use the value of .libPaths()) to some other location where knitr is not available. You can't accomplish this by overwriting base::require(), nor (in a package) can you get there by defining a local masking version of require() with the desired value of lib.loc.

It does, though, look like you can get away with using trace() to temporarily modify require() (blinding it to knitr's availability by setting lib.loc=R.home()). Then do untrace() to restore require() to the vanilla version which will go ahead and find knitr.

Here's what that looked like in the dummy package I tested this with. First an R function that allows us to test for success along the two branches

## $PKG_SRC/R/hello.R

hello <- function(x=1) {
    if(require(knitr)) {
        x==2
    } else {
        x==3
    }
}

Then a couple of tests, one for each branch:

## $PKG_SRC/inst/tests/testme.R

## Test the second branch, run when require(knitr) fails
trace("require", quote(lib.loc <- R.home()), at=1)
stopifnot(hello(3))
untrace("require")

## Test the first branch, run when require(knitr) succeeds
stopifnot(hello(2))

To test this, I used pkgKitten::kitten("dummy") to set up a source directory, copied in these two files, added Suggests: knitr to the DESCRIPTION file, and then ran devtools::install() and devtools::check() from the appropriate directory. The package installs just fine, and passes all of the checks.

like image 153
Josh O'Brien Avatar answered Sep 19 '22 13:09

Josh O'Brien