I am trying to use SmallCheck to test a Haskell program, but I cannot understand how to use the library to test my own data types. Apparently, I need to use the Test.SmallCheck.Series. However, I find the documentation for it extremely confusing. I am interested in both cookbook-style solutions and an understandable explanation of the logical (monadic?) structure. Here are some questions I have (all related):
If I have a data type
data Person = SnowWhite | Dwarf Integer
, how do I explain tosmallCheck
that the valid values areDwarf 1
throughDwarf 7
(orSnowWhite
)? What if I have a complicatedFairyTale
data structure and a constructormakeTale :: [Person] -> FairyTale
, and I wantsmallCheck
to make FairyTale-s from lists of Person-s using the constructor?I managed to make
quickCheck
work like this without getting my hands too dirty by using judicious applications ofControl.Monad.liftM
to functions likemakeTale
. I couldn't figure out a way to do this withsmallCheck
(please explain it to me!).What is the relationship between the types
Serial
,Series
, etc.?(optional) What is the point of
coSeries
? How do I use thePositive
type fromSmallCheck.Series
?(optional) Any elucidation of what is the logic behind what should be a monadic expression, and what is just a regular function, in the context of smallCheck, would be appreciated.
If there is there any intro/tutorial to using smallCheck
, I'd appreciate a link. Thank you very much!
UPDATE: I should add that the most useful and readable documentation I found for smallCheck
is this paper (PDF). I could not find the answer to my questions there on the first look; it is more of a persuasive advertisement than a tutorial.
UPDATE 2: I moved my question about the weird Identity
that shows up in the type of Test.SmallCheck.list
and other places to a separate question.
While I think that @tel's answer is an excellent explanation (and I wish
smallCheck
actually worked the way he describes), the code he provides does not work for me (withsmallCheck
version 1). I managed to get the following to work...... but I find the use of
Control.Monad.Identity
and all the compiler flags bizarre, and I have asked a separate question about that.Note also that while
Series Person
(or actuallySeries Identity Person
) is not actually exactly the same as functionsDepth -> [Person]
(see @tel's answer), the functiongenerate :: Depth -> [a] -> Series m a
converts between them.NOTE: This answer describes pre-1.0 versions of SmallCheck. See this blog post for the important differences between SmallCheck 0.6 and 1.0.
SmallCheck is like QuickCheck in that it tests a property over some part of the space of possible types. The difference is that it tries to exhaustively enumerate a series all of the "small" values instead of an arbitrary subset of smallish values.
As I hinted, SmallCheck's
Serial
is like QuickCheck'sArbitrary
.Now
Serial
is pretty simple: aSerial
typea
has a way (series
) to generate aSeries
type which is just a function fromDepth -> [a]
. Or, to unpack that,Serial
objects are objects we know how to enumerate some "small" values of. We are also given aDepth
parameter which controls how many small values we should generate, but let's ignore it for a minute.In these cases we're doing nothing more than ignoring the
Depth
parameter and then enumerating "all" possible values for each type. We can even do this automatically for some typesThis is a really simple way of testing properties exhaustively—literally test every single possible input! Obviously there are at least two major pitfalls, though: (1) infinite data types will lead to infinite loops when testing and (2) nested types lead to exponentially larger spaces of examples to look through. In both cases, SmallCheck gets really large really quickly.
So that's the point of the
Depth
parameter—it lets the system ask us to keep ourSeries
small. From the documentation,Depth
is theso let's rework our examples to keep them Small.
Much better.
So what's
coseries
? Likecoarbitrary
in theArbitrary
typeclass of QuickCheck, it lets us build a series of "small" functions. Note that we're writing the instance over the input type---the result type is handed to us in anotherSerial
argument (that I'm below callingresults
).these take a little more ingenuity to write and I'll actually refer you to use the
alts
methods which I'll describe briefly below.So how can we make some
Series
ofPerson
s? This part is easyBut our
coseries
function needs to generate every possible function fromPerson
s to something else. This can be done using thealtsN
series of functions provided by SmallCheck. Here's one way to write itThe basic idea is that
altsN results
generates aSeries
ofN
-ary function fromN
values withSerial
instances to theSerial
instance ofResults
. So we use it to create a function from [0..7], a previously definedSerial
value, to whatever we need, then we map ourPerson
s to numbers and pass 'em in.So now that we have a
Serial
instance forPerson
, we can use it to build more complex nestedSerial
instances. For "instance", ifFairyTale
is a list ofPerson
s, we can use theSerial a => Serial [a]
instance alongside ourSerial Person
instance to easily create aSerial FairyTale
:(the
(makeFairyTale .)
composesmakeFairyTale
with each functioncoseries
generates, which is a little confusing)data Person = SnowWhite | Dwarf Integer
, how do I explain tosmallCheck
that the valid values areDwarf 1
throughDwarf 7
(orSnowWhite
)?First of all, you need to decide which values you want to generate for each depth. There's no single right answer here, it depends on how fine-grained you want your search space to be.
Here are just two possible options:
people d = SnowWhite : map Dwarf [1..7]
(doesn't depend on the depth)people d = take d $ SnowWhite : map Dwarf [1..7]
(each unit of depth increases the search space by one element)After you've decided on that, your
Serial
instance is as simple asWe left
m
polymorphic here as we don't require any specific structure of the underlying monad.FairyTale
data structure and a constructormakeTale :: [Person] -> FairyTale
, and I wantsmallCheck
to make FairyTale-s from lists of Person-s using the constructor?Use
cons1
:Serial
,Series
, etc.?Serial
is a type class;Series
is a type. You can have multipleSeries
of the same type — they correspond to different ways to enumerate values of that type. However, it may be arduous to specify for each value how it should be generated. TheSerial
class lets us specify a good default for generating values of a particular type.The definition of
Serial
isSo all it does is assigning a particular
Series m a
to a given combination ofm
anda
.coseries
?It is needed to generate values of functional types.
Positive
type fromSmallCheck.Series
?For example, like this:
When you are writing a
Serial
instance (or anySeries
expression), you work in theSeries m
monad.When you are writing tests, you work with simple functions that return
Bool
orProperty m
.