JCheck.jl Documentation
What is JCheck.jl?
JCheck is a test framework for the Julia programming language. It aims at imitating the one and only Quickcheck. The user specifies a set of properties in the form of predicates. JCheck then tries to falsify these predicates. Since it is in general impossible to evaluate a predicate for every possible input, JCheck (as does QuickCheck) employs a Monte Carlo approach: it samples a set of inputs at random and passes them as arguments to the predicates. To analyze problematic cases more conveniently, Serialization to a JLSO file is enabled by default.
Features
- Reuse inputs to cut into the time dedicated to cases generation.
- Serialization of problematic cases for convenient analysis.
- Integration with Julia's testing framework.
- Allow specification of "special cases" i.e. non-random inputs that are always checked.
- Shrinkage of failing test cases.
Usage
Container
Predicates must be contained in a Quickcheck
object to be used in a test. Those are easy to create. The most basic way is to call the constructor with a short and simple description:
qc = Quickcheck("A Test")
A Test: 0 predicate and 0 free variable.
For more advanced usages, see documentation of the Quickcheck
constructor.
Adding predicates
Once a Quickcheck
object has been created, the next step is to populate it with predicates. This can be done using the @add_predicate
macro:
@add_predicate qc "Sum commute" ((x::Float64, n::Int) -> x + n == n + x)
A Test: 1 predicate and 2 free variables:
n::Int64
x::Float64
A predicate is a function that returns either true
or false
. In the context of JCheck
the form of the predicate is strict; please read the documentation of @add_predicate
.
(Quick)checking
The macro @quickcheck
launches the process of looking for falsifying instances in a Quickcheck
object.
@quickcheck qc
Test Summary: | Pass Total
Test Sum commute | 1 1
As part of a @testset
The @quickcheck
macro can be nested inside @testset
. This allows easy integration to a package's set of tests.
@testset "Sample test set" begin
@test isempty([])
@quickcheck qc
end
Test Summary: | Pass Total
Sample test set | 2 2
Let's add a failing predicate.
@add_predicate qc "I fail" (x::Float64 -> false)
@testset "Sample failing test set" begin
@test isempty([])
@quickcheck qc
end
┌ Warning: Predicate "I fail" does not hold for valuation (x = 0.0,)
└ @ JCheck ~/Projets/JCheck/src/Quickcheck.jl:267
┌ Warning: Predicate "I fail" does not hold for valuation (x = 1.0,)
└ @ JCheck ~/Projets/JCheck/src/Quickcheck.jl:267
[...]
Some predicates do not hold for some valuations; they have been saved
to JCheck_yyyy-mm-dd_HH-MM-SS.jchk. Use function load and macro @getcases
to explore problematic cases.
Test Summary: | Pass Fail Total
Sample failing test set | 2 1 3
Test Sum commute | 1 1
Test I fail | 1 1
ERROR: Some tests did not pass: 2 passed, 1 failed, 0 errored, 0 broken.
Analysing failing cases
By default, failing test cases are serialized to a JLSO file so they can easily be analyzed.
ft = JCheck.load("JCheck_test.jchk")
2 failing predicates:
Product commute
Is odd
Failing cases for a predicate can be extracted using its description with @getcases
. There is no need to give the exact description of the predicate you want to extract; the entry which description is closest to the one given (in the sense of the Levenshtein distance) will be matched.
pred, valuations = @getcases ft i od
## Each element of `valuations` is a tuple.
map(x -> pred(x...), valuations)
3-element Vector{Bool}:
0
0
0
Types with built-in generators
For a list of types for which a generator is included in the package, see reference for generate
.
Testing With Custom Types
JCheck can easily be extended to work with custom types from which it is possible to randomly sample instances. The only requirement is to overload generate
. For instance, an implementation for the type Int64
could look like this:
import JCheck: generate
using Random: AbstractRNG
generate(rng::AbstractRNG, ::Type{Int64}, n::Int) =
rand(rng, Int64, n)
generate (generic function with 33 methods)
Optionally, it is possible to specify so-called "special cases" for a type. Those are always checked. Doing so is as easy as overloading specialcases
. For Int
, this could look like this:
import JCheck: specialcases
specialcases(::Type{Int64}) =
Int64[0, 1, typemin(Int64), typemax(Int64)]
specialcases (generic function with 29 methods)
For implementation details, see the documentation of these two functions.
Shrinkage
@quickcheck
will try to shrink any failing test case if possible. In order to enable shrinkage for a given type, the following two methods must be implemented:
The first one is a predicate evaluating to true
for an object if it can be shrinked. The second is a function returning a Vector
of shrunk objects. The implementation for type Abstractstring
is the following:
shrinkable(x::AbstractString) = length(x) >= 2
function shrink(x::AbstractString)
shrinkable(x) || return typeof(x)[x]
n = length(x) ÷ 2
[x[1:n], x[range(n + 1, end)]]
end
shrink (generic function with 1 method)
For more details and a list of default shrinkers, see the documentation of these methods.