I’ve created a small library called hspec-quickcheck-classes, for testing typeclass laws within Hspec specifications.
It’s a tiny library (with just one function), but I’d be very grateful for any feedback before I release it on Hackage.
What problem does it solve?
This library integrates Hspec with quickcheck-classes. For those not familiar:
-
Hspecis a very popular Haskell testing framework that provides a readable DSL for structuring tests. -
quickcheck-classesis a library that provides QuickCheck property tests for the laws of typeclasses such asEq,Ord,Functor,Applicative,Monad, and many others. It defines aLawsdata type, which bundles together a list of named properties for a single typeclass.
Using quickcheck-classes within an Hspec specification requires a small but repetitive amount of boilerplate.
This library handles this boilerplate for you, constructing a Spec from a given type and a set of Laws. The generated Spec consists of a tree with an outer node for the type, an inner node for each typeclass, and a leaf for each law being tested (where each law has a corresponding QuickCheck property):
<type> ← outer node (type)
├── <typeclass #a> ← inner node (typeclass)
│ ├── <law #1> ← leaf (law)
│ ├── <law #2>
│ └── ...
├── <typeclass #b>
│ ├── <law #1>
│ ├── <law #2>
│ └── ...
└── <typeclass #c>
├── <law #1>
├── <law #2>
└── ...
...
The API
The library exposes just a single function:
testLaws
:: forall a. (Typeable a, HasCallStack)
=> [Proxy a -> Laws]
-> Spec
Usage looks like this:
import Test.Hspec
( hspec )
import Test.Hspec.QuickCheck.Classes
( testLaws )
import Test.QuickCheck
( Arbitrary (..) )
import Test.QuickCheck.Classes
( eqLaws, ordLaws, showLaws )
-- Import the data type you'd like to test:
import Data.Foo
( Foo (..) )
-- Define (or import) an 'Arbitrary' instance for your data type:
instance Arbitrary Foo where
arbitrary = ...
shrink = ...
main :: IO ()
main = hspec $ do
-- Test that your data type obeys the laws of 'Eq', 'Ord', and 'Show':
testLaws @Foo
[ eqLaws
, ordLaws
, showLaws
]
This produces output along the lines of:
Testing laws for Foo
Eq
Transitive [✔]
+++ OK, passed 100 tests.
Symmetric [✔]
+++ OK, passed 100 tests.
Reflexive [✔]
+++ OK, passed 100 tests.
Ord
Antisymmetry [✔]
+++ OK, passed 100 tests.
Transitivity [✔]
+++ OK, passed 100 tests.
Totality [✔]
+++ OK, passed 100 tests.
Show
Show [✔]
+++ OK, passed 100 tests.
Equivariance: showsPrec [✔]
+++ OK, passed 100 tests.
Equivariance: showList [✔]
+++ OK, passed 100 tests.
On failure, the output includes a counterexample, and the location of the test that failed in the test source code.
For example, here’s a failing test taken from the monoidmap package, showing a violation of the LeftReductive laws (due to a deliberately-introduced bug):
test/Data/MonoidMap/Internal/ClassSpec.hs:92:9:
1) Data.MonoidMap, Testing laws for MonoidMap String String, LeftReductive, leftReductiveLaw_stripPrefix
Falsified (after 20 tests and 10 shrinks):
Property not satisfied:
maybe b (a <>) (stripPrefix a b) == b
Values:
a =
fromList []
b =
fromList [("A","a")]
stripPrefix a b =
Just (fromList [("A","a")])
maybe b (a <>) (stripPrefix a b) =
fromList []
Kind polymorphism
To support testing of laws for higher-kinded classes like Functor, Applicative, and Traversable, the testLaws function is kind-polymorphic, with no special casing required.
For example, with Maybe, which has kind Type -> Type:
testLaws @Maybe
[ applicativeLaws
, functorLaws
, monadLaws
, foldableLaws
, traversableLaws
]
And Either, which has kind Type -> Type -> Type:
testLaws @Either
[ bifoldableLaws
, bifunctorLaws
, bitraversableLaws
]
Feedback
Obviously this is a tiny library, but if anyone has any feedback they’d like to share about the API, the documentation, or the implementation, I’d be very grateful to hear your thoughts before the library is published.
Many thanks for reading!