Title: | Property-Based Testing |
---|---|
Description: | Hedgehog will eat all your bugs. 'Hedgehog' is a property-based testing package in the spirit of 'QuickCheck'. With 'Hedgehog', one can test properties of their programs against randomly generated input, providing far superior test coverage compared to unit testing. One of the key benefits of 'Hedgehog' is integrated shrinking of counterexamples, which allows one to quickly find the cause of bugs, given salient examples when incorrect behaviour occurs. |
Authors: | Huw Campbell [aut, cre] |
Maintainer: | Huw Campbell <[email protected]> |
License: | MIT + file LICENSE |
Version: | 0.1 |
Built: | 2024-12-12 06:28:47 UTC |
Source: | https://github.com/hedgehogqa/r-hedgehog |
This helper function assists one in creating commands for state machine testing in hedgehog.
command( title, generator, execute, require = function(state, ...) T, update = function(state, output, ...) state, ensure = function(state, output, ...) NULL )
command( title, generator, execute, require = function(state, ...) T, update = function(state, output, ...) state, ensure = function(state, output, ...) NULL )
title |
the name of this command, to be shown when reporting any failing test cases. |
generator |
A generator which provides random arguments for the command, given the current (symbolic) state. If nothing can be done with the current state, one should preclude the situation with a requires and return NULL. Otherwise, it should be a list of arguments (the empty list is ok for functions which take no arguments). |
execute |
A function from the concrete input, which executes the true function and returns concrete output. Function takes the (possibly named) arguments given by the generator. |
require |
A function from the current (symbolic) state to a bool, indicating if action is currently applicable. Function also takes the (possibly named) arguments given by the generator (this is mostly used in shrinking, to ensure after a shrink its still something which could have been generated by the function generator). |
update |
A function from state to state, which is polymorphic over symbolic and concrete inputs and outputs (as it is used in both action generation and command execution). It's critical that one doesn't "inspect" the output and input values when writing this function. |
ensure |
A post-condition for a command that must be verified for the command to be considered a success. This should be a set of testthat expectations. |
a command structure.
Executes the list of commands sequentially, ensuring that all postconditions hold.
expect_sequential(initial.state, actions)
expect_sequential(initial.state, actions)
initial.state |
the starting state to build from which is appropriate for this state machine generator. |
actions |
the list of actions which are to be run. |
an expectation.
Check a property holds for all generated values.
forall( generator, property, tests = getOption("hedgehog.tests", 100), size.limit = getOption("hedgehog.size", 50), shrink.limit = getOption("hedgehog.shrinks", 100), discard.limit = getOption("hedgehog.discards", 100), curry = identical(class(generator), "list") )
forall( generator, property, tests = getOption("hedgehog.tests", 100), size.limit = getOption("hedgehog.size", 50), shrink.limit = getOption("hedgehog.shrinks", 100), discard.limit = getOption("hedgehog.discards", 100), curry = identical(class(generator), "list") )
generator |
a generator or list of generators (potentially nested) to use for value testing. |
property |
a function which takes a value from the generator and tests some predicated against it. |
tests |
the number of tests to run |
size.limit |
the max size used for the generators |
shrink.limit |
the maximum number of shrinks to run when shrinking a value to find the smallest counterexample. |
discard.limit |
the maximum number of discards to permit when running the property. |
curry |
whether to curry the arguments passed
to the property, and use do.call to use the list
generated as individual arguments.
When curry is on, the function arity should be the
same as the length of the generated list.
Defaults to |
The generator used can be defined flexibly, in that one can pass in a list of generators, or even nest generators and constant values deeply into the gen argument and the whole construct will be treated as a generator.
test_that( "Reverse and concatenate symmetry", forall( list( as = gen.c( gen.element(1:100) ) , bs = gen.c( gen.element(1:100) )) , function( as, bs ) expect_identical ( rev(c(as, bs)), c(rev(bs), rev(as))) ) ) # False example showing minimum shrink: ## Not run: test_that( "Reverse is identity", forall ( gen.c( gen.element(1:100)), function(x) { expect_identical ( rev(x), c(x) ) } ) ) ## End(Not run) # Falsifiable after 1 tests, and 5 shrinks # Predicate is falsifiable # Counterexample: # [1] 1 2
test_that( "Reverse and concatenate symmetry", forall( list( as = gen.c( gen.element(1:100) ) , bs = gen.c( gen.element(1:100) )) , function( as, bs ) expect_identical ( rev(c(as, bs)), c(rev(bs), rev(as))) ) ) # False example showing minimum shrink: ## Not run: test_that( "Reverse is identity", forall ( gen.c( gen.element(1:100)), function(x) { expect_identical ( rev(x), c(x) ) } ) ) ## End(Not run) # Falsifiable after 1 tests, and 5 shrinks # Predicate is falsifiable # Counterexample: # [1] 1 2
Generators which sample from a list or produce random
integer samples. Both single sample, with gen.element
;
and multi-sample, with gen.sample
and gen.subsequence
are supported; while gen.choice
is used to choose from
generators instead of examples.
gen.element(x, prob = NULL) gen.int(n, prob = NULL) gen.choice(..., prob = NULL) gen.subsequence(x) gen.sample(x, size, replace = FALSE, prob = NULL) gen.sample.int(n, size, replace = FALSE, prob = NULL)
gen.element(x, prob = NULL) gen.int(n, prob = NULL) gen.choice(..., prob = NULL) gen.subsequence(x) gen.sample(x, size, replace = FALSE, prob = NULL) gen.sample.int(n, size, replace = FALSE, prob = NULL)
x |
a list or vector to sample an element from. |
prob |
a vector of probability weights for obtaining the elements of the vector being sampled. |
n |
the number which is the maximum integer sampled from. |
... |
generators to sample from |
size |
a non-negative integer or a generator of one, giving the number of items to choose. |
replace |
Should sampling be with replacement? |
These generators implement shrinking.
gen.element
returns an item from the list
or vector; gen.int
, an integer up to the value
n; gen.choice
, a value from one of given selected
generators; gen.subsequence
an ordered subsequence
from the input sequence; and gen.sample
a list or
vector (depending on the input) of the inputs.
For gen.element
and gen.choice
, shrinking
will move towards the first item; gen.int
will
shrink to 1; gen.subsequence
will shrink the list
towards being empty; and gen.sample
will shrink
towards the original list order.
gen.element(1:10) # a number gen.element(c(TRUE,FALSE)) # a boolean gen.int(10) # a number up to 10 gen.choice(gen.element(1:10), gen.element(letters)) gen.choice(NaN, Inf, gen.unif(-10, 10), prob = c(1,1,10)) gen.subsequence(1:10)
gen.element(1:10) # a number gen.element(c(TRUE,FALSE)) # a boolean gen.int(10) # a number up to 10 gen.choice(gen.element(1:10), gen.element(letters)) gen.choice(NaN, Inf, gen.unif(-10, 10), prob = c(1,1,10)) gen.subsequence(1:10)
A Hedgehog generator is a function, which, using R's random seed, will build a lazy rose tree given a size parameter, which represent a value to test, as well as possible shrinks to try in the event of a failure. Usually, one should compose the provided generators instead of dealing with the gen contructor itself.
gen(t) gen.and_then(g, f) gen.bind(f, g) gen.pure(x) gen.impure(fg) gen.with(g, m) gen.map(m, g)
gen(t) gen.and_then(g, f) gen.bind(f, g) gen.pure(x) gen.impure(fg) gen.with(g, m) gen.map(m, g)
t |
a function producing a tree from a size parameter, usually an R function producing random values is used. |
g |
a generator to map or bind over |
f |
a function from a value to new generator, used to build new generators monadically from a generator's output |
x |
a value to use as a generator |
fg |
a function producing a single value from a size parameter |
m |
a function to apply to values produced the generator |
Hedgehog generators are functors and monads, allowing one to map over them and use their results to create more complex generators.
A generator can use R's random seed when constructing its value, but all shrinks should be deterministic.
In general, functions which accept a generator can also be provided with a list of generators nested arbitrarily.
Generators which are created from impure values (i.e., have
randomness), can be created with gen.impure
,
which takes a function from size
to a value. When
using this the function will not shrink, so it is best
composed with gen.shrink
.
generate
for way an alternative, but
equally expressive way to compose generators using R's
"for" loop.
# Create a generator which produces a number between # 1 and 30 one_to_30 <- gen.element(1:30) # Use this to create a simple vector of 6 numbers # between 1 and 30. vector_one_to_30 <- gen.c(of = 6, one_to_30) # Create a matrix 2 by 3 matrix using said vector gen.map(function(x) matrix(x, ncol=3), vector_one_to_30) # To create a generator from a normal R random function # use gen.impure (this generator does not shrink). g <- gen.impure(function(size) sample(1:10) ) gen.example(g) # [1] 5 6 3 4 8 10 2 7 9 1 # Composing generators with `gen.bind` and `gen.with` is # easy. Here we make a generator which first build a length, # then, elements of that length. g <- gen.bind(function(x) gen.c(of = x, gen.element(1:10)), gen.element(2:100)) gen.example ( g ) # [1] 8 6 2 7 5 4 2 2 4 6 4 6 6 3 6 7 8 5 4 6
# Create a generator which produces a number between # 1 and 30 one_to_30 <- gen.element(1:30) # Use this to create a simple vector of 6 numbers # between 1 and 30. vector_one_to_30 <- gen.c(of = 6, one_to_30) # Create a matrix 2 by 3 matrix using said vector gen.map(function(x) matrix(x, ncol=3), vector_one_to_30) # To create a generator from a normal R random function # use gen.impure (this generator does not shrink). g <- gen.impure(function(size) sample(1:10) ) gen.example(g) # [1] 5 6 3 4 8 10 2 7 9 1 # Composing generators with `gen.bind` and `gen.with` is # easy. Here we make a generator which first build a length, # then, elements of that length. g <- gen.bind(function(x) gen.c(of = x, gen.element(1:10)), gen.element(2:100)) gen.example ( g ) # [1] 8 6 2 7 5 4 2 2 4 6 4 6 6 3 6 7 8 5 4 6
Generate a list of possible actions.
gen.actions(initial.state, commands)
gen.actions(initial.state, commands)
initial.state |
the starting state to build from which is appropriate for this state machine generator. |
commands |
the list of commands which we can select choose from. Only commands appropriate for the state will actually be selected. |
a list of actions to run during testing
Shrinks towards the median value.
gen.beta(shape1, shape2, ncp = 0)
gen.beta(shape1, shape2, ncp = 0)
shape1 |
same as shape1 in rbeta |
shape2 |
same as shape2 in rbeta |
ncp |
same as ncp in rbeta |
Generate a vector of values from a generator
gen.c(generator, from = 1, to = NULL, of = NULL)
gen.c(generator, from = 1, to = NULL, of = NULL)
generator |
a generator used for vector elements |
from |
minimum length of the list of elements |
to |
maximum length of the list of elements (defaults to size if NULL) |
of |
the exact length of the list of elements (exclusive to 'from' and 'to'). |
Shrinks towards the from
value.
gen.date(from = as.Date("1900-01-01"), to = as.Date("3000-01-01"))
gen.date(from = as.Date("1900-01-01"), to = as.Date("3000-01-01"))
from |
a |
to |
a |
gen.date() gen.date( from = as.Date("1939-09-01"), to = as.Date("1945-09-02"))
gen.date() gen.date( from = as.Date("1939-09-01"), to = as.Date("1945-09-02"))
Sample from a generator.
gen.example(g, size = 5)
gen.example(g, size = 5)
g |
A generator |
size |
The sized example to view |
Shrinks towards the median value.
gen.gamma(shape, rate = 1, scale = 1/rate)
gen.gamma(shape, rate = 1, scale = 1/rate)
shape |
same as shape in rgamma |
rate |
same as rate in rgamma |
scale |
same as scale in rgamma |
Generate a list of values, with length bounded by the size parameter.
gen.list(generator, from = 1, to = NULL, of = NULL)
gen.list(generator, from = 1, to = NULL, of = NULL)
generator |
a generator used for list elements |
from |
minimum length of the list of elements |
to |
maximum length of the list of elements ( defaults to size if NULL ) |
of |
the exact length of the list of elements (exclusive to 'from' and 'to'). |
Stop a generator from shrinking
gen.no.shrink(g)
gen.no.shrink(g)
g |
a generator we wish to remove shrinking from |
This will choose between the recursive and non-recursive terms, while shrinking the size of the recursive calls.
gen.recursive(tails, heads)
gen.recursive(tails, heads)
tails |
a list of generators which should not contain recursive terms. |
heads |
a list of generator which may contain recursive terms. |
# Generating a tree with integer leaves treeGen <- gen.recursive( # The non-recursive cases list( gen.int(100) ) , # The recursive cases list( gen.list( treeGen ) ) )
# Generating a tree with integer leaves treeGen <- gen.recursive( # The non-recursive cases list( gen.int(100) ) , # The recursive cases list( gen.list( treeGen ) ) )
Samples from a generator or list of generators producing a (single) lazy rose tree.
gen.run(generator, size)
gen.run(generator, size)
generator |
A generator |
size |
The size parameter passed to the generation functions |
This is different to calling generarator$unGen(size) in that it also works on (nested) lists of generators and pure values.
shrinker takes an 'a and returns a vector of 'a.
gen.shrink(shrinker, g)
gen.shrink(shrinker, g)
shrinker |
a function takes an 'a and returning a vector of 'a. |
g |
a generator we wish to add shrinking to |
Helper for making a gen with a size parameter. Pass a function which takes an int and returns a gen.
gen.sized(f)
gen.sized(f)
f |
the function, taking a size and returning a generator |
gen.sized ( function(e) gen.element(1:e) )
gen.sized ( function(e) gen.element(1:e) )
If you can create an object with structure
,
you should be able to generate an object with
this function from a generator or list of
generators.
gen.structure(x, ...)
gen.structure(x, ...)
x |
an object generator which will have various attributes attached to it. |
... |
attributes, specified in 'tag = value' form, which will be attached to generated data. |
gen.structure accepts the same forms of data as forall, and is flexible, in that any list of generators is considered to be a generator.
# To create a matrix gen.structure( gen.c(of = 6, gen.element(1:30)), dim = 3:2) # To create a data frame for testing. gen.structure ( list ( gen.c(of = 4, gen.element(2:10)) , gen.c(of = 4, gen.element(2:10)) , c('a', 'b', 'c', 'd') ) , names = c('a','b', 'constant') , class = 'data.frame' , row.names = c('1', '2', '3', '4' ))
# To create a matrix gen.structure( gen.c(of = 6, gen.element(1:30)), dim = 3:2) # To create a data frame for testing. gen.structure ( list ( gen.c(of = 4, gen.element(2:10)) , gen.c(of = 4, gen.element(2:10)) , c('a', 'b', 'c', 'd') ) , names = c('a','b', 'constant') , class = 'data.frame' , row.names = c('1', '2', '3', '4' ))
Shrinks towards the from
value, or
if shrink.median
is on, the middle.
gen.unif(from, to, shrink.median = T)
gen.unif(from, to, shrink.median = T)
from |
same as from in runif |
to |
same as to in runif |
shrink.median |
whether to shrink to the middle of the distribution instead of the low end. |
gen.unif(0, 1) # a float between 0 and 1
gen.unif(0, 1) # a float between 0 and 1
Use 'generator' with a for loop over the output of another generator to create a new, more interesting generator.
generate(loop)
generate(loop)
loop |
A 'for' loop expression, where the value iterated over is another Hedgehog generator. |
[gen-monad()] for FP style ways of sequencing generators. This function is syntactic sugar over 'gen.and_then' to make it palatable for R users.
gen_squares <- generate(for (i in gen.int(10)) i^2) gen_sq_digits <- generate(for (i in gen_squares) { gen.c(of = i, gen.element(1:9)) })
gen_squares <- generate(for (i in gen.int(10)) i^2) gen_sq_digits <- generate(for (i in gen_squares) { gen.c(of = i, gen.element(1:9)) })
Hedgehog is a modern property based testing system in the spirit of QuickCheck, originally written in Haskell, but now also available in R.
Software testing is critical when we want to distribute our work, but unit testing only covers examples we have thought of.
With hedgehog (integrated into testthat), we can instead test properties which our programs and functions should have, and allow automatic generation of tests, which cover more that we could imagine.
One of the key benefits of Hedgehog is integrated shrinking of counterexamples, which allows one to quickly find the cause of bugs, given salient examples when incorrect behaviour occurs.
- 'hedgehog.tests': Number of tests to run in each property (Default: '100').
- 'hedgehog.size': Maximum size parameter to pass to generators (Default: '50').
- 'hedgehog.shrinks': Maximum number of shrinks to search for (Default: '100').
- 'hedgehog.discards': Maximum number of discards permitted within a property test before failure (Default: '100').
Campbell, H (2017). hedgehog: Property based testing in R The R Journal under submission.
https://github.com/hedgehogqa/r-hedgehog
library(hedgehog) test_that( "Reverse and concatenate symmetry", forall( list( as = gen.c( gen.element(1:100) ) , bs = gen.c( gen.element(1:100) )) , function( as, bs ) expect_identical ( rev(c(as, bs)), c(rev(bs), rev(as))) ) )
library(hedgehog) test_that( "Reverse and concatenate symmetry", forall( list( as = gen.c( gen.element(1:100) ) , bs = gen.c( gen.element(1:100) )) , function( as, bs ) expect_identical ( rev(c(as, bs)), c(rev(bs), rev(as))) ) )
Shrink a number by dividing it into halves.
shrink.halves(x)
shrink.halves(x)
x |
number to produce halves of |
shrink.towards(45) # 22 11 5 2 1
shrink.towards(45) # 22 11 5 2 1
Shrink a list by edging towards the empty list.
shrink.list(xs)
shrink.list(xs)
xs |
the list to shrink |
Produce permutations of removing num elements from a list.
shrink.removes(num, xs)
shrink.removes(num, xs)
num |
the number of values to drop |
xs |
the list to shrink |
Note we always try the destination first, as that is the optimal shrink.
shrink.towards(destination)
shrink.towards(destination)
destination |
the value we want to shrink towards. |
shrink.towards (0) (100) # [0,50,75,88,94,97,99] shrink.towards(500)(1000) # [500,750,875,938,969,985,993,997,999] shrink.towards (-50) (-26) # [-50,-38,-32,-29,-27]
shrink.towards (0) (100) # [0,50,75,88,94,97,99] shrink.towards(500)(1000) # [500,750,875,938,969,985,993,997,999] shrink.towards (-50) (-26) # [-50,-38,-32,-29,-27]
These values are the outputs of a computation during the calculations' construction, and allow a value to use the results of a previous function.
symbolic(var)
symbolic(var)
var |
the integer output indicator. |
Really, this is just an integer, which we use as a name for a value which will exist later in the computation.
A rose tree is a type of multibranch tree. This is hedgehog's internal implementation of a lazy rose tree.
tree(root, children_ = list()) tree.map(f, x) tree.bind(f, x) tree.liftA2(f, x, y) tree.expand(shrink, x) tree.unfold(shrink, a) tree.unfoldForest(shrink, a) tree.sequence(trees)
tree(root, children_ = list()) tree.map(f, x) tree.bind(f, x) tree.liftA2(f, x, y) tree.expand(shrink, x) tree.unfold(shrink, a) tree.unfoldForest(shrink, a) tree.sequence(trees)
root |
the root of the rose tree |
children_ |
a list of children for the tree. |
f |
a function for mapping, binding, or applying |
x |
a tree to map or bind over |
y |
a tree to map or bind over |
shrink |
a shrinking function |
a |
a value to unfold from |
trees |
a tree, or list or structure potentially containing trees to turn into a tree of said structure. |
In general, one should not be required to use any of the functions from this module as the combinators in the gen module should be expressive enough (if they're not raise an issue).
Build a tree of a list, potentially keeping hold of an internal state.
tree.replicate(num, ma) tree.replicateS(num, ma, s, ...)
tree.replicate(num, ma) tree.replicateS(num, ma, s, ...)
num |
the length of the list in the tree |
ma |
a function which (randomly) creates new tree to add to the list |
s |
a state used when replicating to keep track of. |
... |
extra arguments to pass to the tree generating function |