A lightweight enumeration-based property testing library.
Tinycheck is a deterministic property testing library. Instead of random generation, test cases are produced by exhaustive, fairly-interleaved enumeration. It integrates with the Tasty test framework via Test.Tasty.TinyCheck.
tinycheck
A lightweight, deterministic property testing library for Haskell.
Instead of generating random inputs, tinycheck enumerates test cases from a canonical, fairly-interleaved ordering. Tests are reproducible, require no seeds, and cover small values first — no shrinking required.
Quick start
import Test.Tasty
import Test.Tasty.TinyCheck
main :: IO ()
main = defaultMain $ testGroup "my suite"
[ testProperty "reverse . reverse == id" $
\(xs :: [Int]) -> reverse (reverse xs) == xs
, testProperty "abs x >= 0" $
\(x :: Int) -> abs x >= 0
]
Run with cabal test.
How it works
The core type is TestCases a — a newtype over [a] whose Semigroup, Applicative, and Monad instances use fair interleaving instead of concatenation and cartesian product.
TestCases [1,2,3] <> TestCases [10,20,30] == TestCases [1,10,2,20,3,30]
With infinite generators this ensures neither side is starved:
(Left <$> TestCases [1..]) <> (Right <$> TestCases [1..])
== TestCases [Left 1, Right 1, Left 2, Right 2, ...]
Applicative interleaves function-argument pairs, so all parts of the input space are explored immediately rather than exhausting one argument before moving to the next.
Use interleaveN to interleave any number of generators fairly:
interleaveN [TestCases [1,2,3], TestCases [10,20,30], TestCases [100,200,300]]
== TestCases [1,10,100, 2,20,200, 3,30,300]
Defining generators
Implement Arbitrary for your types, or derive it via Generically:
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics (Generic, Generically (..))
import Data.TestCases (Arbitrary)
data Colour = Red | Green | Blue
deriving stock (Show, Generic)
deriving (Arbitrary) via Generically Colour
Newtype wrappers are provided for common patterns:
| Wrapper | Suitable for |
|---|---|
SignedArbitrary | Num + Enum (e.g. Int, Integer) |
BoundedArbitrary | Bounded + Enum (e.g. Bool, Word8) |
RealFracArbitrary | Fractional + Enum (e.g. Float, Double) |
| ... | ... |
Preconditions
Use ==> to skip inputs that don't satisfy a precondition:
testProperty "n > 0 implies n * 2 > 0" $
\(n :: Int) -> (n > 0) ==> property (n * 2 > 0)
Development
Modules
| Module | Purpose |
|---|---|
Data.TestCases | Core TestCases type, Arbitrary, CoArbitrary |
Test.Tasty.TinyCheck | Tasty integration (testProperty, testPropertyWith, …) |
examples/
Two standalone examples for defining Arbitrary instances for your own types are provided here.
AI usage
The central pieces are developed by a human, @turion. Many details (tasty integration, boilerplate, test cases, longer docs) have been generated by AI (Github Copilot with Claude Opus & Sonnet 4.6) and are thoroughly checked by myself.