Property based testing in R, inspired by QuickCheck. This package builds on the property based testing framework provided by hedgehog
and is designed to seamlessly integrate with testthat
.
You can install the released version of quickcheck
from CRAN with:
install.packages("quickcheck")
And the development version from GitHub with:
# install.packages("remotes") remotes::install_github("armcn/quickcheck")
The following example uses quickcheck
to test the properties of the base R +
function. Here is an introduction to the concept of property based testing, and an explanation of the mathematical properties of addition can be found here.
library(testthat) library(quickcheck) test_that("0 is the additive identity of +", { for_all( a = numeric_(len = 1), property = function(a) expect_equal(a, a + 0) ) }) #> Test passed π test_that("+ is commutative", { for_all( a = numeric_(len = 1), b = numeric_(len = 1), property = function(a, b) expect_equal(a + b, b + a) ) }) #> Test passed π test_that("+ is associative", { for_all( a = numeric_(len = 1), b = numeric_(len = 1), c = numeric_(len = 1), property = function(a, b, c) expect_equal(a + (b + c), (a + b) + c) ) }) #> Test passed π
Here we test the properties of the distinct
function from the dplyr
package.
library(dplyr, warn.conflicts = FALSE) test_that("distinct does nothing with a single row", { for_all( a = any_tibble(rows = 1L), property = function(a) { distinct(a) %>% expect_equal(a) } ) }) #> Test passed π₯³ test_that("distinct returns single row if rows are repeated", { for_all( a = any_tibble(rows = 1L), property = function(a) { bind_rows(a, a) %>% distinct() %>% expect_equal(a) } ) }) #> Test passed π test_that("distinct does nothing if rows are unique", { for_all( a = tibble_of(integer_positive(), rows = 1L, cols = 1L), b = tibble_of(integer_negative(), rows = 1L, cols = 1L), property = function(a, b) { unique_rows <- bind_rows(a, b) distinct(unique_rows) %>% expect_equal(unique_rows) } ) }) #> Test passed π
Many generators are provided with quickcheck
. Here are a few examples.
integer_(len = 10) %>% show_example() #> [1] 1645 -8572 -9846 5213 -4605 -3086 296 -7463 4333 3471 character_alphanumeric(len = 10) %>% show_example() #> [1] "V6" "P" "G" "pu" "aEIIEU6d3" "jDiV4" "6" "hqHX" "Pe2Eejmkk" "xU3dKuw" posixct_(len = 10, any_na = TRUE) %>% show_example() #> [1] "1518-05-23 19:00:11 LMT" "2037-05-06 23:31:16 EDT" NA NA #> [5] NA "1406-01-03 06:03:02 LMT" "2002-12-12 22:52:53 EST" "1196-12-09 06:22:30 LMT" #> [9] "0631-11-28 09:04:35 LMT" "2682-01-16 11:51:32 EST"
list_(a = constant(NULL), b = any_undefined()) %>% show_example() #> $a #> NULL #> #> $b #> [1] NA flat_list_of(logical_(), len = 3) %>% show_example() #> [[1]] #> [1] FALSE #> #> [[2]] #> [1] TRUE #> #> [[3]] #> [1] FALSE
tibble_(a = date_(), b = hms_(), rows = 5) %>% show_example() #> # A tibble: 5 Γ 2 #> a b #> <date> <time> #> 1 2971-02-26 15:59:18.485111 #> 2 1259-02-26 09:17:34.134997 #> 3 1719-12-02 02:35:26.647900 #> 4 2186-06-14 18:59:36.013421 #> 5 2005-05-16 22:58:25.777202 tibble_of(double_bounded(-10, 10), rows = 3, cols = 3) %>% show_example() #> # A tibble: 3 Γ 3 #> ...1 ...2 ...3 #> <dbl> <dbl> <dbl> #> 1 -8.75 -1.33 3.63 #> 2 5.49 -5.71 0.755 #> 3 0 3.42 -3.07 any_tibble(rows = 3, cols = 3) %>% show_example() #> # A tibble: 3 Γ 3 #> ...1 ...2 ...3 #> <dbl> <time> <list> #> 1 -885519673. 14:05:05.882342 <chr [1]> #> 2 -293069776. 04:11:51.356973 <chr [1]> #> 3 -90043652. 21:32:00.080639 <chr [1]>
quickcheck
is meant to work with hedgehog
, not replace it. hedgehog
generators can be used by wrapping them in from_hedgehog
.
library(hedgehog) is_even <- function(a) a %% 2 == 0 gen_powers_of_two <- gen.element(1:10) %>% gen.with(function(a) 2^a) test_that("is_even returns TRUE for powers of two", { for_all( a = from_hedgehog(gen_powers_of_two), property = function(a) is_even(a) %>% expect_true() ) }) #> Test passed πΈ
Any hedgehog
generator can be used with quickcheck
but they canβt be composed together to build another generator. For example this will work:
test_that("powers of two and integers are both numeric values", { for_all( a = from_hedgehog(gen_powers_of_two), b = integer_(), property = function(a, b) { c(a, b) %>% is.numeric() %>% expect_true() } ) }) #> Test passed π
But this will cause an error:
test_that("composing hedgehog with quickcheck generators fails", { tibble_of(from_hedgehog(gen_powers_of_two)) %>% expect_error() }) #> Test passed π
A quickcheck
generator can also be converted to a hedgehog
generator which can then be used with other hedgehog
functions.
gen_powers_of_two <- integer_bounded(1L, 10L, len = 1L) %>% as_hedgehog() %>% gen.with(function(a) 2^a) test_that("is_even returns TRUE for powers of two", { for_all( a = from_hedgehog(gen_powers_of_two), property = function(a) is_even(a) %>% expect_true() ) }) #> Test passed π
Fuzz testing is a special case of property based testing in which the only property being tested is that the code doesnβt fail with a range of inputs. Here is an example of how to do fuzz testing with quickcheck
. Letβs say we want to test that the purrr::map
function wonβt fail with any vector as input.
test_that("map won't fail with any vector as input", { for_all( a = any_vector(), property = function(a) purrr::map(a, identity) %>% expect_silent() ) }) #> Test passed πΈ
Repeat tests can be used to repeatedly test that a property holds true for many calls of a function. These are different from regular property based tests because they donβt require generators. The function repeat_test
will call a function many times to ensure the expectation passes in all cases. This kind of test can be useful for testing functions with randomness.
test_that("runif generates random numbers between a min and max value", { repeat_test( property = function() { random_number <- runif(1, min = 0, max = 10) expect_true(random_number >= 0 && random_number <= 10) } ) }) #> Test passed π
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4