Jack's love of dice has brought him here, where he has taken on the form of a PureScript library, in order to help you gamble with your propositions.
Jack is a testing library in the spirit of Hughes & Classen's QuickCheck.
What makes it different is that instead of generating a random value and using a shrinking function after the fact, we generate the random value and all the possible shrinks in a rose tree, all at once.
Generating the shrinks when you generate your initial value has many advantages.
It is easy to maintain the invariants of a generator for example. With
QuickCheck style shrinking if you do
chooseInt 100 200 and then try to
shrink it, it will happily shrink to
QuickCheck shrinking functions are also invariant, as the
a appears on
both sides of the arrow in the shrink function
a -> List a.
So if you imagine the following scenario:
newtype Foo = Foo String shrinkString :: String -> List String shrinkString = ...
shrinkString cannot be lifted to work over
Foo without having
a mapping in both directions. This breaks the beautiful applicative
syntax that generators can be constructed with. Jack doesn't have this
Gen String can be turned in to a
Gen Foo using only
map, but with the benefit that your
Foo will be shrunk for free.
The easiest way to get started with Jack is to install it as a dev dependency:
$ bower install --save-dev purescript-jack
Then create a
test/Main.purs as shown below:
module Test.Main ( main ) where import Control.Monad.Eff (Eff) import Control.Monad.Eff.Console (CONSOLE) import Control.Monad.Eff.Random (RANDOM) import Jack.Runner (jackMain) import Prelude main :: forall e. Eff ("random" :: RANDOM, "console" :: CONSOLE | e) Unit main = jackMain [ -- List all the modules which contain property tests here, for this -- example we have just one module: Test.DiceGames "Test.DiceGames" ]
Jack has a test runner, invoked via
jackMain, which scans the given
modules for properties. Any exported function of type
prop_ will be found and exercised.
Lets take a look at
test/DiceGames.purs for a contrived example:
module Test.DiceGames where import Data.Array as Array import Data.Foldable (elem) import Data.Maybe (Maybe(..)) import Data.String as String import Jack (Property, Gen, property, forAll, boundedInt) import Prelude genEven :: Gen Int genEven = map (\x -> (x / 2) * 2) boundedInt genEvenString :: Gen String genEvenString = map show genEven evens :: Array Char evens = String.toCharArray "02468" prop_even_strings_end_with_evens :: Property prop_even_strings_end_with_evens = forAll genEvenString \str -> case Array.last $ String.toCharArray str of Nothing -> property false -- numbers should always have at least one digit Just x -> property $ elem x evens
Here we're verifying that even numbers always end with an even digit. We do this by generating random even numbers as strings and checking that the last character is what we expect.
If we run this example using
pulp test we get the following output:
$ pulp test * Building project in /home/jack/example * Build successful. * Running tests... === prop_even_strings_end_with_evens from Test.DiceGames === +++ OK, passed 100 tests. * Tests OK.
Let's sabotage the test so that we're generating odd numbers instead of evens:
genEven :: Gen Int genEven = map (\x -> (x / 2) * 2 + 1) boundedInt -- note the + 1
And run our example again with
$ pulp test * Building project in /home/jack/example * Build successful. === prop_even_strings_end_with_evens from Test.Foo === *** Failed! Falsifiable (after 1 test and 1 shrink): "1" * ERROR: Subcommand terminated with exit code 1
You can see we get a failure, and after shrinking we're presented with
the minimal possible counterexample,
Note that we didn't have to do any extra work to get shrinking of our even or odd numbers. Even shrinking a string which must be a constrained number happens automatically. With traditional QuickCheck shrinking we would have had to parse the string, shrink the number, check that's it's still even (or odd), and then convert it back to a string again.
Many of the features you'd expect from a QuickCheck library are still missing, but we'll get there eventually:
- Monadic / effectful tests
- Generating functions
- Model testing
Jack doesn't have an
Arbitrary type class, by design.
instances often end up being orphans and I consider this problematic,
especially in PureScript. The main purpose of the
Arbitrary class as
I see it is to link the generator with a shrink function, this isn't
required with Jack so
Arbitrary has been eliminated.
This library is still very new, and I wouldn't be surprised if some of the combinators are still a bit buggy. Jack relies heavily on laziness and it wasn't something that I had to worry about when I wrote it for Haskell, so I'm fixing stack overflows and out of memory errors as they appear.
Ensure PureScript is installed and available on your path. Getting started instructions are available on the PureScript site.
bower to install the PureScript packages:
pulp to build
pulp build pulp test