Search code examples
rautomated-testsdevtoolstestthatrstan

R package development: tests pass in console, but fail via devtools::test()


I am developing an R package that calls functions from the package rstan. As a MWE, my test file is currently set up like this, using code taken verbatim from rstan's example:

library(testthat)
library(rstan)

# stan's own example
stancode <- 'data {real y_mean;} parameters {real y;} model {y ~ normal(y_mean,1);}'
mod <- stan_model(model_code = stancode, verbose = TRUE)
fit <- sampling(mod, data = list(y_mean = 0))
# I added this line, and it's the culprit
summary(fit)$summary

When I run this code in the console or via the "Run Tests" button in RStudio, no errors are thrown. However, when I run devtools::test(), I get:

Error (test_moments.R:11:1): (code run outside of `test_that()`)
Error in `summary(fit)$summary`: $ operator is invalid for atomic vectors

and this error is definitely not occurring upstream of that final line of code, because removing the final line allows devtools::test() to run without error. I am running up-to-date packages devtools and rstan.


Solution

  • It seems that devtools::test evaluates the test code in a setting where S4 dispatch does not work in the usual way, at least for packages that you load explicitly in the test file (in this case rstan). As a result, summary dispatches to summary.default instead of the S4 method implemented in rstan for class "stanfit".

    The behaviour that you're seeing might relate to this issue on the testthat repo, which seems unresolved.

    Here is a minimal example that tries to illuminate what is happening, showing one possible (admittedly inconvenient) work-around.

    pkgname <- "foo"
    usethis::create_package(pkgname, rstudio = FALSE, open = FALSE)
    setwd(pkgname)
    
    usethis::use_testthat()
    path_to_test <- file.path("tests", "testthat", "test-summary.R")
    text <- "test_that('summary', {
      library('rstan')
      stancode <- 'data {real y_mean;} parameters {real y;} model {y ~ normal(y_mean,1);}'
      mod <- stan_model(model_code = stancode, verbose = TRUE)
      fit <- sampling(mod, data = list(y_mean = 0))
    
      expect_identical(class(fit), structure('stanfit', package = 'rstan'))
      expect_true(existsMethod('summary', 'stanfit'))
    
      x <- summary(fit)
      expect_error(x$summary)
      expect_identical(x, summary.default(fit))
      print(x)
    
      f <- selectMethod('summary', 'stanfit')
      y <- f(fit)
      str(y)
    })
    "
    cat(text, file = path_to_test)
    
    devtools::test(".") # all tests pass
    

    If your package actually imports rstan (in the NAMESPACE sense, not in the DESCRIPTION sense), then S4 dispatch seems to work fine, presumably because devtools loads your package and its dependencies in a "proper" way before running any tests.

    cat("import(rstan)\n", file = "NAMESPACE")
    newtext <- "test_that('summary', {
      stancode <- 'data {real y_mean;} parameters {real y;} model {y ~ normal(y_mean,1);}'
      mod <- stan_model(model_code = stancode, verbose = TRUE)
      fit <- sampling(mod, data = list(y_mean = 0))
    
      x <- summary(fit)
      f <- selectMethod('summary', 'stanfit')
      y <- f(fit)
      expect_identical(x, y)
    })
    "
    cat(newtext, file = path_to_test)
    
    ## You must restart your R session here. The current session 
    ## is contaminated by the previous call to 'devtools::test',
    ## which loads packages without cleaning up after itself...
    
    devtools::test(".") # all tests pass
    

    If your test is failing and your package imports rstan, then something else may be going on, but it is difficult to diagnose without a minimal version of your package.

    Disclaimer: Going out of your way to import rstan to get around a relatively obscure devtools issue should be considered more of a hack than a fix, and documented accordingly...