Isolating packages in tests and documentation examples

I'm finishing writing tests and documentation for a package that generates R package citations on the fly in R Markdown/Quarto.

For example, the following .Rmd/.qmd

---
bibliography: foo.bib
---

We created maps using `r bar::fun(ggplot2)`.

# References

will produce

We created maps using the 'ggplot2' package version 3.4.4 (Wickham 2016).

References

Wickham, Hadley. 2016. *Ggplot2: Elegant Graphics for Data Analysis*.
Springer-Verlag New York. <https://ggplot2.tidyverse.org>.

I'm using a few snapshot tests and use similar examples in the README and pkgdown documentation.

The problem is that the tests/examples will change whenever I update the package used in the examples/tests. By 'examples' I mean examples in the README + pkgdown vignettes, not functions' help file.

What are the solutions to work around this? I only need to use one package for all my examples and tests so I'd prefer something that isn't overly complicated. Ideally, I'd like to have no extra dependencies for this to keep my package as minimal as possible.

testthat::expect_snapshot(transform =) works fine for simple cases when testing but is of no help for the examples in the documentation. It's also not super convenient when snapshot testing bibTex citations.

I was thinking of using renv but I've no idea how to use it for that purpose inside a package, supposing that's actually possible. There's a vignette about package development but don't find it very helpful.

Another possibility might be to create a temporary package locally but that's probably overkill.

Any hints or recommendations?

How about installing a specific version of a minimal package like {xfun} (no dependencies and no compiled code) into a temporary directory and using this for the tests? This is the basic idea of what I do here for my package {OmicNavigator}. Here is some example code:

# Setup
tmplib <- tempfile()
dir.create(tmplib)
libOrig <- .libPaths()
.libPaths(c(tmplib, libOrig))
remotes::install_version("xfun", version = "0.9")

# Test code here
packageVersion("xfun")
## [1] ‘0.9’

# Teardown
unlink(tmplib, recursive = TRUE, force = TRUE)
.libPaths(libOrig)

# Does not affect system installation
packageVersion("xfun")
## [1] ‘0.42’

And for the examples, you shouldn't need an exact version match. Thus I'd recommend using one of your package's Imports (so that you know it will always be installed when your example code is run).

I actually like this idea. You could create a super simple package in inst/ that would be installed with your tests. The nice thing about this is that it will isolate your tests from random connection failures when downloading a test package from CRAN.

1 Like

Thanks. I like the simplicity of this solution but having to install a remote package is prone to connection failures as you said, which is not ideal.

For the local package I was thinking of doing something like this:

local_pkg <- function(metadata, pattern = "testpkg", env = parent.frame()) {
  dir <- withr::local_tempfile(pattern = pattern, .local_envir = env)
  usethis::ui_silence(
    usethis::create_package(
      dir,
      rstudio = FALSE,
      open = FALSE,
      fields = metadata
    )
  )
  invisible(dir)
}

pkg <- local_pkg(list(
  # Package = "Foo",
  Title = "An Example Package",
  Version = "1.0.0",
  `Authors@R` = utils::person("X", "Y", role = c("aut", "cre"))
))

utils::packageVersion(basename(pkg), lib.loc = dirname(pkg))
#> [1] '1.0.0'

utils::citation(basename(pkg), lib.loc = dirname(pkg)) |> 
  format(style = "bibtex") |>
  cat()
#> Warning in utils::citation(basename(pkg), lib.loc = dirname(pkg)): could not
#> determine year for 'testpkg11b8850c6bfde' from package DESCRIPTION file
#> @Manual{,
#>   title = {testpkg11b8850c6bfde: An Example Package},
#>   author = {X Y},
#>   note = {R package version 1.0.0},
#> }

Created on 2024-03-12 with reprex v2.1.0

This has the advantage to make it very easy to create multiple packages for my tests/examples. My only issue is to set the name of the package. If I name the package explicitly e.g. local_pkg(list(Package = "Foo")), citation() won't find it, even if I rename the path with:

file.rename(pkg, paste0(dirname(pkg), "/Foo"))

Edit:
In fact I can simply do:

utils::packageVersion("Foo", lib.loc = dirname(pkg))

utils::citation("Foo", lib.loc = dirname(pkg)) |>
    format(style = "bibtex") |>
    cat()

Using a temporary local package was simpler than I thought so I guess I'll go for this solution.

The only problem with documentation's examples is the README. If I update the README after getting a newer version of the package I use in the examples, they will also change. This is not a big deal in itself, but I don't like the idea of having something changes without me making the change.

The only downside is that I'll have to add usethis as a dependency just for this :frowning:

1 Like

In case that's of any help to someone, here's a simple implementation of a temporary local package:

local_pkg <- function(Package, ..., env = parent.frame()) {
  dir <- withr::local_tempdir(.local_envir = env)
  withr::local_libpaths(dir, .local_envir = env)
  pkg_path <- file.path(dir, Package)
  usethis::ui_silence(
    usethis::create_package(
      path = pkg_path,
      fields = list(Package = Package, ...),
      rstudio = FALSE,
      open = FALSE
    )
  )
}

local_pkg(
  Package = "lorem",
  Version = "2.0.0",
  Date = "2023-01-01",
  Title = "Lorem Ipsum",
  `Authors@R` = utils::person("Foo", "Bar", role = "aut")
)

packageVersion("lorem")
#> [1] '2.0.0'

citation("lorem")
#> To cite package 'lorem' in publications use:
#> 
#>   Bar F (2023). _lorem: Lorem Ipsum_. R package version 2.0.0.
#> 
#> A BibTeX entry for LaTeX users is
#> 
#>   @Manual{,
#>     title = {lorem: Lorem Ipsum},
#>     author = {Foo Bar},
#>     year = {2023},
#>     note = {R package version 2.0.0},
#>   }

Created on 2024-03-13 with reprex v2.1.0

2 Likes

I agree! I like your solution

The good news is that this is a development-only dependency, so you can add it to Suggests (ie your users don't have to install it).

Also, just an FYI, you don't need to use {usethis} to create and install a package. To create the package, you could use utils::package.skeleton(). Or since you only need a DESCRIPTION file to test for the citation, you could get away with dir.create() and write.dcf(). And to install a local package, you can use install.packages("path/to/local_pkg", repos = NULL).

2 Likes

I remember reading something saying that packages used in vignettes should be listed in the Imports field when articles only require to have them in Suggests. Apart from tests, I'll only use temp. packages in the README which I'm not sure what category it falls into (probably vignette). I'll try with Suggests and see if R Cmd Check complains.

Thanks for the tip. I'll probably do that as a fallback if need be.

1 Like

It's ultimately up to you, but for a leaner installation, I highly recommend putting all non-essential packages in Suggests.

From Writing R Extensions:

The ‘Suggests’ field uses the same syntax as ‘Depends’ and lists packages that are not necessarily needed. This includes packages used only in examples, tests or vignettes

From R Packages (2e):

Suggests : your package can use these packages, but doesn’t require them. You might use suggested packages for example datasets, to run tests, build vignettes, or maybe there’s only one function that needs the package.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.