Writing client-side tests are more complicated than writing server-side tests. Whilst server-side tests call local functions, client-side tests check that the client-side function successfully calls the server-side function and receives the expected results. In this tutorial, we will use DSLite to run the client-side tests. The advantage of this approach is that it is easier to setup and run within the testthat
flow. The disadvantage is that the behaviour of DSLite may not mirror exactly the behaviour of a real-life scenario. You can view an alternative setup using real servers here.
The following is written as a separate script named tests/testthat/setupDSLite.R
.
library(DSLite)
library(devtools)
setupDSLite <- function() {
options(datashield.env = environment())
dslite.server <- DSLite::newDSLiteServer()
load_all("~/Library/Mobile Documents/com~apple~CloudDocs/work/dsExample")
dslite.server$config(defaultDSConfiguration(include=c("dsBase", "dsExample")))
dslite.server$aggregateMethod("funLevelsDS", "funLevelsDS")
dslite.server$aggregateMethod("listDisclosureSettingsDS", "listDisclosureSettingsDS")
builder <- DSI::newDSLoginBuilder()
builder$append(
server = "server_1",
url = "dslite.server",
driver = "DSLiteDriver"
)
logindata <- builder$build()
conns <- DSI::datashield.login(logins = logindata, assign = FALSE)
return(conns)
}
setupDSLite <- function() {
Define a function which can be called in your test scripts.
options(datashield.env = environment())
Set the DataSHIELD environment to be the global environment. It is necessary for DSLite to work with devtools::check()
and devtools::test()
.
load_all("~/Library/Mobile Documents/com~apple~CloudDocs/work/dsExample")
Load the server-side package so it can be used by DSLite.
dslite.server$config(defaultDSConfiguration(include=c("dsBase", "dsExample")))
dslite.server$aggregateMethod("funLevelsDS", "funLevelsDS")
dslite.server$aggregateMethod("listDisclosureSettingsDS", "listDisclosureSettingsDS")
Specify the server-side functions that can be used.
builder <- DSI::newDSLoginBuilder()
builder$append(
server = "server_1",
url = "dslite.server",
driver = "DSLiteDriver"
)
logindata <- builder$build()
conns <- DSI::datashield.login(logins = logindata, assign = FALSE)
return(conns)
Login to DSLite server.
This file tests the the main function ds.funLevels.R
.
library(testthat)
conns <- setupDSLite()
test_that("Valid inputs do not throw an error", {
expect_silent(.check_args("example", "This is a message"))
expect_silent(.check_args(c("a", "b"), "Message"))
})
test_that("NULL x throws an error", {
expect_error(.check_args(NULL, "This is a message"), "`x` must not be NULL")
})
test_that("NULL fun_message throws an error", {
expect_error(.check_args("example", NULL), "`fun_message` must not be NULL")
})
test_that("Non-character x throws an error", {
expect_error(.check_args(123, "This is a message"), "`x` must be a character vector")
expect_error(.check_args(TRUE, "This is a message"), "`x` must be a character vector")
expect_error(.check_args(list("a", "b"), "This is a message"), "`x` must be a character vector")
})
test_that("Non-character fun_message throws an error", {
expect_error(.check_args("example", 123), "`fun_message` must be a character vector")
expect_error(.check_args("example", FALSE), "`fun_message` must be a character vector")
expect_error(.check_args("example", list("a")), "`fun_message` must be a character vector")
})
test_that("ds.funLevels returns the correct message", {
expect_equal(
ds.funLevels(
x = "iris$Species",
fun_message = "ThisIsAFunMessage",
datasources = conns),
list(server_1 = "ThisIsAFunMessage: setosa, versicolor, virginica")
)
})
test_that("ds.funLevels returns an error if factor has too many levels", {
expect_error(
ds.funLevels(
x = "mtcars$disp",
fun_message = "ThisIsAFunMessage",
datasources = conns)
)
})
conns <- setupDSLite()
Call the function that we defined to set up the DSLite server.
test_that("Valid inputs do not throw an error", {
expect_silent(.check_args("example", "This is a message"))
expect_silent(.check_args(c("a", "b"), "Message"))
})
Test that if character vectors are provided to x
and fun_message
no error is thrown.
test_that("NULL x throws an error", {
expect_error(.check_args(NULL, "This is a message"), "`x` must not be NULL")
})
test_that("NULL fun_message throws an error", {
expect_error(.check_args("example", NULL), "`fun_message` must not be NULL")
})
Test that an error is thrown if either x
or fun_message
is null.
test_that("Non-character x throws an error", {
expect_error(.check_args(123, "This is a message"), "`x` must be a character vector")
expect_error(.check_args(TRUE, "This is a message"), "`x` must be a character vector")
expect_error(.check_args(list("a", "b"), "This is a message"), "`x` must be a character vector")
})
test_that("Non-character fun_message throws an error", {
expect_error(.check_args("example", 123), "`fun_message` must be a character vector")
expect_error(.check_args("example", FALSE), "`fun_message` must be a character vector")
expect_error(.check_args("example", list("a")), "`fun_message` must be a character vector")
})
Test that an error if thrown if either x
or fun_message
is not a character.
test_that("ds.funLevels returns the correct message", {
expect_equal(
ds.funLevels(
x = "iris$Species",
fun_message = "ThisIsAFunMessage",
datasources = conns),
list(server_1 = "ThisIsAFunMessage: setosa, versicolor, virginica")
)
})
Test that ds.funLevels returns the correct message in a valid use case.
test_that("ds.funLevels returns an error if factor has too many levels", {
expect_error(
ds.funLevels(
x = "mtcars$disp",
fun_message = "ThisIsAFunMessage",
datasources = conns)
)
})
Test that ds.funLevels returns an error if the number of levels in the factor is too high.
This tests additional functions included in the utils.R file.
library(testthat)
test_that(".check_datasources works with valid input", {
if (!isClass("MockDSConnection")) {
setClass(
"MockDSConnection",
contains = "DSConnection",
representation(dummy = "character"))
}
conn1 <- new("MockDSConnection", dummy = "a")
conn2 <- new("MockDSConnection", dummy = "b")
expect_silent(.check_datasources(list(conn1, conn2)))
})
test_that(".check_datasources throws error with non-list input", {
expect_error(
.check_datasources("not a list"),
"'Datasources' must be a list of objects with class `DSConnection`")
})
test_that(".check_datasources throws error with list containing invalid elements", {
conn <- new("MockDSConnection", dummy = "valid")
expect_error(
.check_datasources(list(conn, "not valid")),
"'Datasources' must be a list of objects with class `DSConnection`")
})
test_that(".check_datasources works with valid input", {
if (!isClass("MockDSConnection")) {
setClass(
"MockDSConnection",
contains = "DSConnection",
representation(dummy = "character"))
}
conn1 <- new("MockDSConnection", dummy = "a")
conn2 <- new("MockDSConnection", dummy = "b")
expect_silent(.check_datasources(list(conn1, conn2)))
})
Create test objects of class DSConnection
and test that no error is thrown.
test_that(".check_datasources throws error with non-list input", {
expect_error(
.check_datasources("not a list"),
"'Datasources' must be a list of objects with class `DSConnection`")
})
Test that an error is thrown if the object provided is a character.
test_that(".check_datasources throws error with list containing invalid elements", {
conn <- new("MockDSConnection", dummy = "valid")
expect_error(
.check_datasources(list(conn, "not valid")),
"'Datasources' must be a list of objects with class `DSConnection`")
})
Test that an error if thrown is one element of the list is the correct class and the other not.