'testthat: set up database connection available to all tests

My R package modifies data in a remote DB and I'd like to write some tests with testthat.

I am aware that I could mock the DB but I'd rather simply use of one our dev DB.

How can I make a db connection available to all tests that need it, while making sure any connection created is destroyed? It seems obvious that connecting should happen in a setup and disconnecting in a teardown, but I haven't managed.

I have tried to put the following code in tests/testthat.R or in a helper file tests/testthat/helper-_, to no avail.

setup({
  # db_connect is just a basic wrapper around RMariaDB::dbConnect with logging
  db_con <- db_connect(conf$database, loglevel = "none")
})

teardown({
  # db_connect is just a basic wrapper around DBI::dbDisconnect with logging
  db_disconnect(db_con = db_con, loglevel = "none")
})

My initial tests are:

tests
├── testthat
│   ├── helper-_.R
│   ├── test-connect.R
│   └── test-questions.R
└── testthat.R

After the first file (where all tests pass), I get Error in DBI::dbDisconnect(db_con) : object 'db_con' not found which indicates the teardown is happening but the db_con is not found.

After that, all tests requiring db_con fail with object 'db_con' not found.

Do I have to create a helper file for each file where db_con is needed? Or do I have to explicitly source a common helper file?

Is there a way I can set up the connection once somewhere and have it available to all tests and destroyed at the end?



Solution 1:[1]

From the testthat docs

Code in a setup() block is run immediately in a clean environment

I believe this means that if you want to save whatever objects are created in your setup environment, then you need to place them in the global environment

setup({
  db_con <- db_connect(conf$database, loglevel = "none")
  assign("db_con", db_con, envir = .GlobalEnv)
})

Then in your teardown() method, it will be able to find the connection

teardown({
  db_disconnect(db_con = db_con, loglevel = "none")
  # Can also remove it from the global environment after disconnect
  rm(db_con, envir = .GlobalEnv)
})

It's not ideal to mess with the global environment, but as long as you name things carefully and remove them when you're done, it shouldn't be a problem.

It seems like setup() was designed more for reading/writing tempfiles/tempdirs than for creating global objects to be used by all tests, but I could be mistaken.

Helpful example in the wild that I came across while researching this question: https://github.com/ropensci/Rpolyhedra/blob/3675a3a6eb8b2807f26fb2ebc929b9f5072681db/tests/testthat/test_package_lib.R#L7

Solution 2:[2]

EDIT 21-07-07 : As of January 14, 2021 there is the package pool that was made to solve this problem in the following fashion :

pool <- dbPool(
  drv = RMySQL::MySQL(),
  dbname = "shinydemo",
  host = "shiny-demo.csa7qlmguqrf.us-east-1.rds.amazonaws.com",
  username = "guest",
  password = "guest"
)

The goal of the pool package is to abstract away the logic of connection management and the performance cost of fetching a new connection from a remote database


EDIT : dbDisconnect in test-connect_init. This structure works best in workflows that take in data from data base (once or few times).

Disclaimer : what follows has been tested successfully with Impala.

I have opted for the sourcing way, creating a connect_init.R function that is called in script and test :

Organisation

R
??? utils
|   ??? connect_init.R
|   ??? df_import.R
??? clean
|   ??? data_clean.R
tests
??? testthat
?   ??? test-connect.R
?   ??? test-import.R
?   ??? test-clean.R
??? testthat.R

Processes

connect_init.R

connect_init <- function(params) DBI::dbConnect(...)

data_clean.R

[...]
con <- connect_init(params)
rqt <- "select * from db.tab"
dframe <- DBI::dbGetQuery(conn = con, rqt)

# --- when import finished
DBI::dbDisconnect(con)

Tests

test-connect.R

context("test-connect")

test_that("connexion to Impala doable",
        res <- mypkg::connect_init(params)
        testthat::expect_true(attributes(res)$class[1] == "Impala")
        DBI::dbDisconnect(res)
})

test-import.R

context("test-import")

test_that("import from Impala doable", {

        res <- mypkg::df_import(paramsbis)

        testthat::expect_s3_class(object = res, class = "data.frame")
        testthat::expect_true(nrow(res) > 0)
})

Then open and close connexion when used in other tests. I am very interested in other way to test this + feedback on how to improve this part.

Should we store minimal sample data so that non-connexion tests won't fail in case of network/db issues ?

Solution 3:[3]

As of testthat verion 3, setup() and teardown() were deprecated.

It is now recommended to use on.exit() or the withr package for any test fixtures. You can learn more in testthat's vignette('test-fixtures'). If your tests are all in the same file this is even simpler than the setup/teardown method actually.

Essentially this code should appear at the beginning of the file:

db_con <- db_connect(conf$database, loglevel = "none")

#Set connection to terminate after all tests are run    
withr::defer(db_disconnect(db_con = db_con, loglevel = "none"))

You can place all of your tests after.

In my experience, your code should be able to find the db_con object with just these lines when automatic tests are run, but if you run into a problem, you can always build a test environment to avoid using the global environment like this:

#Build environment for any variables need in many tests
testEnv <- new.env()

#Open Connection
testEnv$db_con <- db_connect(conf$database, loglevel = "none")

#Set connection to terminate after all tests are run
withr::defer(db_disconnect(db_con = testEnv$db_con, loglevel = "none"))

#Clear the test environment (This could be added to the statement above)
withr::defer(rm(list=ls(envir = testEnv), envir = testEnv))

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Paul Wildenhain
Solution 2
Solution 3 Lia_G