State-Based Property Testing Tutorial (Part 2 - Vars)
State-Based Property Testing Tutorial (Part 2 - Vars)#
This tutorial continues on from the first tutorial.
Follow Along#
Please feel to play along and tinker with the code.
The Problem#
To start with let's define an interface/abstraction for something we want to
test against, part of a basic create/read/update/delete or CRUD store, which
I'm calling CR as we only support create/read to simplify the example:
NOTE: The big difference here from the previous KV is that the service is
returning it's own identifier instead of being passed on. That has implications
for testing the state with Hedgehog, which require something extra...
Model (take 1)#
Similar to our previous example, this also seems fairly straight forward. But we're about to hit a snag...
And our command input types:
Now let's step throught the Command functions for create/read like
before:
Gen#
Generation is fairly typical. Note that we can't really usefully create a
Read value until we have at least one value in State.
Execute (take 1)#
So far everything lines up just like before:
Update#
Hang on, what's that Var[CRid] doing?!? Why isn't it just a concrete CRId?
That code doesn't even compile!
WTF#
It turns out that Hedgehog forces a very clear separation for functions that
can access the "real world" state, and those that can't. The reasons for this
are a little involved, but the basic idea is that we can generate a full
sequence of commands up-front, without having called execute. So the
question/problem then becomes, how do we capture the "result" to be used in
gen, like we need to do for read?
This is where Var and Environment come in. For each Command.execute,
Hedgehog allocates a unique Name (just a plain old incrementing Int), and
passed that to the "pure" functions, like update and gen. It's only when
we need the concrete values do we get access to the Environment to look
them up.
Here is the relevant data types, that should hopefully make more sense now?
Model (take 2)#
So we will need to tweak our model and input slightly to use Var:
Execute (take 2)#
So let's try that again. Note that we now use Environment in the Read
version to get the concrete CRId returned from create.
Ensure#
This is identical to KV, although notice we could use Environment here if
we needed to, unlike update.
Debugging#
Let's look at the output of a failed test again:
We can start to see that each command is shown to be assigning the result to a
new, unique Var. We aren't looking at the actual CRId values. Hedgehog
(currently) can't tell that Read doesn't actually return anything (useful)
and so the Unit value is also asigned to a new Var.
Resources#
Unfortunately there isn't much documentation for this approach at the moment.
- Finding Race Conditions in Erlang with QuickCheck and PULSE Hedgehog follows a vague description of this implementation which is mentioned in this paper.
- State machine testing with Hedgehog
- Haskell Differences