Skip to content

Testing with zazr-test

com.guizmaii:zazr-test checks properties against random inputs. You state what must hold for every value; it generates many values and reports the first one that breaks the rule.

It works in any test framework, such as JUnit: a check is a method call that returns a result you assert on.

<dependency>
    <groupId>com.guizmaii</groupId>
    <artifactId>zazr-test</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>
dependencies {
    testImplementation("com.guizmaii:zazr-test:0.1.0-SNAPSHOT")
}

The types are in com.guizmaii.zazr.test:

Type Role
Gen<T> a generator: a function from a java.util.Random to a T, with map, flatMap, filter, choose, oneOf, frequency
Arbitrary<T> a generator whose values grow with a size: Arbitrary.integer(), string(Gen<Character>), list(Arbitrary), of(values...)
Property the builder: Property.def(name).forAll(arbitraries...).suchThat(predicate), from 1 to 8 arbitraries
CheckResult the outcome: satisfied, falsified (with the sample) or erroneous; assertIsSatisfied() throws an AssertionError otherwise

A property

CheckResult result = Property.def("reversing twice gives the list back")
    .forAll(Arbitrary.list(Arbitrary.integer()))
    .suchThat(list -> list.reverse().reverse().equals(list))
    .check();
result.assertIsSatisfied();

check() tries 1,000 samples with a size of 100: lists of up to 100 elements, integers between -100 and 100. check(size, tries) chooses both. When the property fails, sample() returns the value that broke it.

CheckResult broken = Property.def("every list is short")
    .forAll(Arbitrary.list(Arbitrary.integer()))
    .suchThat(list -> list.length() < 5)
    .check(100, 1_000);
boolean falsified = broken.isFalsified();
// true, and broken.sample() holds the first list of 5 elements or more

Generators

Build a Gen with choose, map, flatMap and the others, then turn it into an Arbitrary with arbitrary().

Gen<Integer> dice = Gen.choose(1, 6);
Arbitrary<Tuple2<Integer, Integer>> pairs = dice.flatMap(a -> dice.map(b -> Tuple.of(a, b))).arbitrary();
CheckResult sums = Property.def("two dice sum to 2..12")
    .forAll(pairs)
    .suchThat(p -> p._1() + p._2() >= 2 && p._1() + p._2() <= 12)
    .check();
sums.assertIsSatisfied();

Preconditions

With implies, the suchThat condition becomes a precondition. Samples that fail it are skipped; the others must satisfy the implies condition.

Checkable halving = Property.def("an even number is twice its half")
    .forAll(Arbitrary.integer())
    .suchThat(n -> n % 2 == 0)
    .implies(n -> (n / 2) * 2 == n);
halving.check().assertIsSatisfied();

Properties combine with and and or.