- Return results using
test.check
’sResult
protocol, rather than custom exceptions. (Thanks to @r0man for doing the hard work here.) - Make mutation detection optional, through the
:run
:assume-immutable-results
option. This delays converting command results into strings until the end of the test run, which may make it easier to provoke some race conditions.To demonstrate the difference between in output, let’s look at this test case:
(def test-atom (atom 0)) (deftest returning-atom-as-result (is (specification-correct? {:commands {:inc {:command #(do (swap! test-atom inc) test-atom) :next-state (fn [s _ _] (inc s)) :postcondition (fn [_ ns _ result] (is (= ns @result)))}} :setup #(reset! test-atom 0) :initial-state (constantly 0)})))
With the default of
:assume-immutable-results
asfalse
we see this output:Sequential prefix: #<4> = (:inc) = #atom[1 0x71f8eaa9] >> object may have been mutated later into #atom[2 0x71f8eaa9] << expected: (= ns @result) actual: (not (= 1 2)) #<5> = (:inc) = #atom[2 0x71f8eaa9]
With
:assume-immutable-results
set totrue
we see this output:Sequential prefix: #<4> = (:inc) = #atom[2 0x71f8eaa9] expected: (= ns @result) actual: (not (= 1 2)) #<5> = (:inc) = #atom[2 0x71f8eaa9]
If you compare the result of command
#<4>
in both cases you can see the difference in output. The assertions in both cases reflect the final value (i.e.actual
is2
in both cases) because postconditions run after the execution phase. - Make the
:command-frequency?
table include a0
count for commands that were not run.
- Add equals/hashCode methods to LookupVars, to allow using the result of
get
on a symbolic value to be a key in a hash map - Resolve symbolic values within arguments, not just at the top level. This allows you to use arguments which are partially generated, and partially results from previous commands. For example:
(def create-user-command {:args (fn [{:keys [user-ids]}] [{:id (gen/elements user-ids), :name gen/string-ascii}]) :command #(update-user %)})
- Fix shrinking of parallel command sequences. Due to a bug, shrinking of parallel command sequences only explored removing a single command, rather than removing pairs of commands. This may have led to poorer shrinks for parallel test cases.
- Add the ability to customise the shrink strategy for a run. The default shrink strategy is the same as before, but you can use the
:gen
:shrink-strategies
option to tune shrinking for your use case. Seestateful-check.shrink-strategies
for the currently available strategies, or you can write your own (although this is undocumented). - Add the ability to use
is
in postconditions to get more specific feedback about failures. The expected/actual values from failing assertions will be captured and printed alongside the command trace, to aid in debugging failures. For example:(def get-command {:requires (fn [state] (seq state)) :args (fn [state] [(gen/elements test-keys)]) :command #(.get system-under-test %1) :postcondition (fn [prev-state _ [k] val] (is (= (get prev-state k) val) (str "Looking up " (pr-str k) " matches the model")))})
This command from the associated test file will give a trace that looks like this:
FAIL in (postcondition-prints-in-parallel-case) (core.clj:211) Sequential prefix: Thread a: #<2a> = (:put "" 0) = nil Thread b: #<1b> = (:put "a" 0) = nil #<4b> = (:get "a") = nil Looking up "a" matches the model expected: (= (get prev-state k) val) actual: (not (= 0 nil)) Seed: 1620654751933 Note: Test cases with multiple threads are not deterministic, so using the same seed does not guarantee the same result. expected: all executions to match specification actual: the above execution did not match the specification
If the postcondition makes any assertions then the return value is not used. This means that the following postcondition will always pass.
{:postcondition (fn [_ _ _ _] (is true) false)}
When performing tests with multiple threads, postcondition failures are a bit tricky to interpret. The postcondition may result from any one (or more) of the interleavings of the commands in the parallel threads.
- Add
:command-frequency?
to the:report
options, to print out a table of how often the test runner executed each command.
- Fix printing of exceptions within postconditions (#12).
- Print out failing seed (#8).
- Add
:timeout-ms
option to fail a test after a certain amount of time (suggestion made after Clj-Syd presentation). - Add forward compatibility for
clojure.test.check
version0.10.0-alpha4
.
Many, many, many changes. I’ll try to go through them.
- Change to the MIT license.
- Remove
real/
andmodel/
prefixes from keys. They don’t mean as much, given the other changes that will be explained below. - Remove the specification
:real/postcondition
function. It doesn’t really fit in the context of parallel tests. - The command execution phase is now separate to the trace verification stage. This means that
:postcondition
functions on commands no longer run interleaves with runs of:command
functions. Now all the:command
functions are run in a sequence, which is then checked by the:postcondition
functions. In particular this means that:postcondition
shouldn’t interact with the SUT at all! - Re-work the options map. It now has three parts:
:gen
,:run
, and:report
. - Add support for running parallel tests, to try to find race conditions. Use
{:gen {:threads 2}}
to generate threads with two parallel threads, and{:run {:max-tries 10}}
to try 10 times to provoke the race condition on each test. - Removed deprecated functions.
:model/args
now coerces the returned values into a generator. Coercion works like the following:- if it’s a generator: return it
- if it’s a sequential collection: coerce each element into a
generator, then use
gen/tuple
to combine them - if it’s a map: coerce each value into a generator, then use
gen/hash-map
to combine each key/value-gen pair - anything else: return it using
gen/return
:model/generate-command
now has a default implementation. If you don’t provide an implementation then it will select a command at random (effectively:(gen/elements (:commands spec))
).- If a value in the
:command
map is a var then dereference it (to facilitate breaking up specs a bit more). - Command results are now printed properly when the results of
commands are mutated. Previously it would print the command results
in their state at the end of the test, irrespective of where they
actually were returned. Now the results will be printed prior to
running the next command in the sequence.
It used to print something like this:
#<0> = (:new) => #{10} #<1> = (:contains? #<0> 10) => false #<2> = (:add #<0> 10) => true #<3> = (:contains? #<0> 10) => true
This incorrectly shows the state of the test (at the point when it was created) to have the element
10
in it. The10
wasn’t added until command#<2>
, however, so that output is incorrect. This could cause us to think the set’s implementation is wrong when it is actually a quirk ofstateful-check
causing this problem.It will now print something like this:
#<0> = (:new) => #{} #<1> = (:contains? #<0> 10) => false #<2> = (:add #<0> 10) => true #<3> = (:contains? #<0> 10) => true
:real/setup
and:real/cleanup
had some major issues (not running being prime among them) which are now fixed. A test has been added to hopefully avoid this happening again in future.- Add a
:tries
argument to thespecification-correct?
options map. This runs each test a number of times, with any failure causing the run to fail. (Useful for non-deterministic tests.) - Shrinking is now a bit more aggressive. In particular, now it will start by trying to shrink single commands (whether by removing the command or by shrinking its arguments), but then it will also try to shrink pairs of commands (removing/shrinking both at the same time). This can lead to dramatically better shrinks in some situations.
- Breaking! Add
next-state
to the:real/postcondition
function arguments in commands.Any command preconditions will need to be modified to take an extra argument.
(fn [state args result] arbitrary-logic-for-postcondition) ;; needs to change to (fn [prev-state next-state args result] arbitrary-logic-for-postcondition)
- Breaking! Change
reality-matches-model?
to be calledreality-matches-model
(it’s not a predicate, so it shouldn’t have a?
in its name). This function is now deprecated, though, in favour of usingdeftest
with our customis
form (see the next point). - Add support for a custom test.check
is
form:(is (specification-correct? some-spec)) (is (specification-correct? some-spec {:num-tests 1000, :max-size 10, :seed 123456789}))
- Make the command generator use the same size for all commands.
- Rewrite the command verifier/runner to make it a whole lot cleaner (including breaking out extra namespaces).
- Upgrade to test.check 0.7.0.
- Tweak the format of
print-test-results
.
- Add namespaces to some keys which didn’t have them before
:generate-command
is now:model/generate-command
:setup
is now:real/setup
:cleanup
is now:real/cleanup
- Add some more keys to the top-level spec object:
:model/initial-state
,:real/initial-state
,:initial-state
for setting the initial state of the system:real/postcondition
on the top-level spec, to check for global invariants
- Make symbolic values implement
ILookup
(to work withget
) - Clean up exception handling during command runs
Initial release.