MyNixOS website logo
Description

A monad for generating QuickCheck properties without Arbitrary instances.

When your data has many invariants, it's often difficult to write Arbitrary instances for QuickCheck. This library attempts to solve that problem by providing a nice interface to write QuickCheck tests without using Arbitrary instances. It aims to be somewhere in the middle between HUnit and QuickCheck: Use the random test case generation of QuickCheck, but write HUnit like assertions.

quickcheck-property-monad

Build Status

Introduction

When your data has many invariants, it's often difficult to write Arbitrary instances for QuickCheck. This library attempts to solve that problem by providing a nice interface to write QuickCheck tests without using Arbitrary instances. It aims to be somewhere in the middle between HUnit and QuickCheck: Use the random test case generation of QuickCheck, but write HUnit like assertions.

A simple model

To show the library in action, let's first create a simple model holding an integer with a one invariant: the value hold by it will always be even. We also provide two operations that preserve the invariant, add2 and multiply, a function to create a new model and a function to check that the invariant holds (we will use this function later when we write our tests):

module Model
  ( Model() -- We don't export the constructor so the invariant cannot be broken.
  , newModel
  , add2
  , multiply
  , checkInvariant
  ) where

data Model = Model { value :: Int } deriving Show

newModel :: Model
newModel = Model 0

add2 :: Model -> Model
add2 (Model x) = Model $ 2 + x

multiply :: Int -> Model -> Model
multiply n (Model x) = Model $ n * x

checkInvariant :: Model -> Bool
checkInvariant (Model x) = even x

Writing tests using the PropM monad

We now want to write tests that ensure that none of our operations will ever break the invariant, no matter in what sequence we apply them. In this case, we could write an Arbitrary instance for our model, but for more complex models, this will quickly become very difficult. Often, you have to use the functions you want to test to create the Arbitrary instance, which means that if the functions are broken, you already generate invalid data to begin with. It's difficult to find the bug in that case.

module Main where

import Model
import Test.QuickCheck
import Test.QuickCheck.Property.Monad

But using quickcheck-property-monad, we can write the tests so that they will fail right after the invariant is broken. First, we define a Gen to generate a random operation. We will also include a short description of the operation, to make debugging easier:

randomOperation :: Gen (String, Model -> Model)
randomOperation = oneof
  [ return ("Add two", add2)
  , fmap (\n -> ("Multiply by " ++ show n, multiply n)) arbitrary
  ]

So far, all functions we've used are provided by QuickCheck itself. Let's now write the test property:

prop_satisfies_invariant :: Property
prop_satisfies_invariant = sized $ \s -> property $ go newModel s
  where go :: Model -> Int -> PropM Bool
        go _     0    = return True
        go model size = do
          (description, operation) <- generate randomOperation
          logMessageLn $ "Operation: " ++ description
          let model' = operation model
          logMessageLn $ "Model is now: " ++ show model'
          assert "Number is even" $ checkInvariant model'
          go model' $ pred size

Here we've used some functions from quickcheck-property-monad. We first grab the size parameter from QuickCheck using sized, and then pass that to go, together with an initial model. go returns a value of type PropM Bool, which we have to convert into a QuickCheck Property using the property function.

But what does go do? First, it looks at the size parameter. If the size is null, we return True, indicating a successful test. If the size is not null, we first generate a random operation, using our previously defined randomOperation function. We use generate to lift the Gen into the PropM monad. After we generated a random operation, we log it. You'll see all messages logged with logMessageLn when the test fails, which is useful for debugging. We then apply the operation on the model. Using assert, we require that the model still satisfies our invariant. assert will do nothing if the condition given to it is True. If it is False, it will abort the test case and report a failure, with the given error message. After that, we recurse, decreasing the size by one so that we eventually reach 0 and stop.

Now, the only function left to write is main:

main :: IO ()
main = quickCheck prop_satisfies_invariant

If we run our test suite, we get the expected output:

+++ OK, passed 100 tests.

All fine!

How a failure looks

Now, if you want to see how a failing test looks like, go back and change

add2 (Model x) = Model $ 2 + x

to

add2 (Model x) = Model $ 1 + x

If we now run our tests, we get a failure, as expected:

*** Failed! Falsifiable (after 2 tests): 
Operation: Add two
Model is now: Model {value = 1}

Assertion failed: Number is even

We get the output from the logMessageLn calls and the message of the assertion that failed.

Contributing

Contributions are always welcome. If you have any ideas, improvements or bug reports, send a pull request or open a issue on github.

Metadata

Version

0.2.4

Platforms (75)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64_be-none
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-darwin
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-darwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-windows