Tinycheck: A lightweight enumeration-based property testing library

I recently started a discussion about the state and future of a library that I like a lot, smallcheck. The maintainer argues that smallcheck is “largely obsolete” in the presence of libraries like falsify. I don’t want to repeat the discussion, if you have an opinion it is warmly welcome at the linked thread. I only mention it because it sparked my interest in the topic.

I toyed around and came up with a very simple way to write an enumeration based property testing library. The core ideas are:

  1. Generators are just streams of values, ordered by complexity (some measure of “size”) of the data.
  2. Generators are combined fairly by interleaving:
interleave :: [a] -> [a] -> [a]
interleave [] as = as
interleave (a : as1) as2 = a : interleave as2 as1

Idea 1 is the basic tenet of enumeration based property testing and has been studied in the literature and is applied in many libraries. No need for random number generation and RNG seeding, no need for shrinking. As a bonus, testing is deterministic.

Idea 2 just makes it really simple to write generators for any kind of algebraic datatype. Generic instances for Arbitrary are fast to write and useful. All kinds of shapes of your data show up early in the test. In contrast to smallcheck, no concept of depth is needed. Generation is very fast and uses little memory. If your test situation needs very specific test data, just interleave it with the standard generator.

I wrapped all of this up and fleshed it out with a bit of AI help (but I checked everything thoroughly). The result is a small library with very few dependencies. There is tasty integration. There are a few examples. So far it seems to work really well. If you are looking for a tiny property testing library, have a look at tinycheck, and let me know what you think.

7 Likes

I think this is the right choice, yes. SmallCheck migrated from this to streams parametrized by a monad (and interleaving implemented via LogicT) between releases 0.6.2 and 1.0, but I don’t know anyone making use of this flexibility. While in theory one can do some cool stuff, e. g., turning SmallCheck into QuickCheck with instance MonadRandom m => Stream m Foo, I’ve never seen it happening in practice, and performance consequences of such generalization are obvious.

2 Likes