Testdat
There are several packages that do data testing, and even a paper about them:
The authors put their list on GitHub if you want to review it.
But, the one I am most familiar with is {testdat}.
Another extension to testthat is testdat, a way to implement data unit testing.
To use it, you can
The standard form of a data expectation is expect_*(var(s), ..., flt = TRUE, data = get_testdata())
Most operations act on one or more variables. There are two variants of the variable argument:
vars requires a set of columns specified as tidy selections.test_that("multi-variable identifier is unique", {
expect_unique(c(name, year, month, day, hour), data = storms)
})
#> ββ Failure: multi-variable identifier is unique ββββββββββββββββββββββββββββββββ
#> `storms` has 48 duplicate records on variable `name, year, month, day, hour`.
#> Filter: None
#> Error:
#> ! Test failedMost operations act on one or more variables. There are two variants of the variable argument:
var requires an unquoted variable name. This only applies to a small number of expectations.test_that("hour values are valid", {
expect_base(ts_diameter, year >= 2004)
})
#> ββ Error: hour values are valid ββββββββββββββββββββββββββββββββββββββββββββββββ
#> Error: A test data frame has not been specified. Use `set_testdata()` to set the data frame.
#> Backtrace:
#> β
#> 1. ββtestdat::expect_base(ts_diameter, year >= 2004)
#> 2. ββtestthat::quasi_label(enquo(data))
#> 3. β ββrlang::eval_bare(expr, quo_get_env(quo))
#> 4. ββtestdat::get_testdata()
#> Error:
#> ! Test failed
You can filter your test data using the flt argument.
test_that("iris range checks", {
expect_range(Petal.Width, 0, 1, data = iris)
})
#> ββ Failure: iris range checks ββββββββββββββββββββββββββββββββββββββββββββββββββ
#> `iris` has 93 records failing range check on variable `Petal.Width`.
#> Variable set: `Petal.Width`
#> Filter: None
#> Arguments: `min = 0, max = 1`
#> Error:
#> ! Test failed
test_that("iris range checks filtered", {
# Test passes for setosa rows
expect_range(Petal.Width, 0, 1, flt = Species == "setosa", data = iris)
# Failures will provide the filter
expect_range(Petal.Width, 0, 0.5, flt = Species == "setosa", data = iris)
})
#> ββ Failure: iris range checks filtered βββββββββββββββββββββββββββββββββββββββββ
#> `iris` has 1 records failing range check on variable `Petal.Width`.
#> Variable set: `Petal.Width`
#> Filter: `Species == "setosa"`
#> Arguments: `min = 0, max = 0.5`
#> Error:
#> ! Test failedYou can use testdat in a script, interactively.
library(testdat)
library(dplyr)
x <- tribble(
~id, ~pcode, ~state, ~nsw_only,
1, 2000, "NSW", 1,
2, 3123, "VIC", NA,
3, 2123, "NSW", 3,
4, 12345, "VIC", 3
)
with_testdata(x, {
test_that("id is unique", {
expect_unique(id)
})
test_that("variable values are correct", {
expect_values(pcode, 2000:2999, 3000:3999)
expect_values(state, c("NSW", "VIC"))
expect_values(nsw_only, 1:3) # by default expect_values allows NAs
})
test_that("filters applied correctly", {
expect_base(nsw_only, state == "NSW")
})
})
x <- x %>% mutate(market = case_when(pcode %in% 2000:2999 ~ 1,
pcode %in% 3000:3999 ~ 2))
with_testdata(x, {
test_that("market derived correctly", {
expect_values(market, 1:2, miss = NULL) # miss = NULL excludes NAs from valid values
})
})Letβs try testdat out interactively with our data. Some expectations you might find useful:
expect_values()expect_range()expect_date_yyymmdd()library(testthat)
library(testdat)
with_testdata(bacon, {
test_that("series ID is correct", {
expect_values(series_id, "APU0000704111")
})
test_that("dates are correct format", {
expect_date_yyymmdd(date)
})
})ββ Failure: dates are correct format βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
`get_testdata()` has 119 records failing YYYYMMDD date format check on variable `date`.
On GitHub
Itβs checking for a string of numbers, without any separation. Our dates have dashes in them.
Luckily, we can make our own expectations! We write a function, and then use expect_make() to turn it into an expectation.
Can you modify
chk_date_yyyymm <- function(x) {
check_lubridate_installed()
chk_blank(x) | (str_detect(x, "[0-9]{6}") & !is.na(lubridate::ymd(paste0(x, "01"), quiet = TRUE)))
}to test for our situation?
library(testthat)
library(testdat)
with_testdata(bacon, {
test_that("series ID is correct", {
expect_values(series_id, "APU0000704111")
})
test_that("dates are correct format", {
expect_date_formatted(date)
})
})Test passed with 1 success π₯³.
Test passed with 1 success π.
Now that we have a couple of tests that work interactively, letβs add them to our test suite. We will need to use a few special files, specifically helper.R and maybe setup.R
There is a usethis helper!
usethis::use_test_helper()
The stuff outside of the test_that() calls should go somewhere other than a standard test-xxx.R file. The tests themselves can live in test-data.R.
test_that("series ID is correct", {
expect_values(series_id, "APU0000704111")
})
# test_that("dates are correct format", {
# expect_date_formatted(date)
# })Edit: custom expectations arenβt working in automated testing.
Edit: custom expectations arenβt working in automated testing
(The library stuff is kind of bad practice, but it took me ages of chasing down an error to figure out. I might put an issue on testdat.)
test()
Now would be a good time to commit your changes
Jason Long, CC BY 3.0 <https://creativecommons.org/licenses/by/3.0>, via Wikimedia Commons
Oops, did we forget to