Arbitrary Version instances with FsCheck by Mark Seemann
This post explains how to configure FsCheck to create arbitrary Version values.
When I unit test generic classes or methods, I often like to use Version as one of the type arguments. The Version class is a great test type because
- it's readily available, as it's defined in the System namespace in mscorlib
- it overrides Equals so that it's easy to compare two values
- it's a complex class, because it composes four integers, so it's a good complement to String, Int32, Object, Guid, and other primitive types
Recently, I've been picking up FsCheck to do Property-Based Testing, but out of the box it doesn't know how to create arbitrary Version instances.
It turns out that you can easily and elegantly tell FsCheck how to create arbitrary Version instances, but since I haven't seen it documented, I thought I'd share my solution:
type Generators = static member Version() = Arb.generate<byte> |> Gen.map int |> Gen.four |> Gen.map (fun (ma, mi, bu, re) -> Version(ma, mi, bu, re)) |> Arb.fromGen
As the FsCheck documentation explains, you can create custom Generator by defining a static class that exposes members that return Arbitrary<'a> - in this case Arbitrary<Version>.
If you'd like me to walk you through what happens here, read on, and I'll break it down for you.
First, Arb.generate<byte>
is a Generator of Byte values. While FsCheck doesn't know how to create arbitrary Version values, it does know how to create arbitrary values of various primitive types, such as Byte, Int32, String, and so on. The Version constructors expect components as Int32 values, so why did I select Byte values instead? Because Version doesn't accept negative numbers, and if I had kicked off my Generator with Arb.generate<int>
, it would have created all sorts of integers, including negative values. While it's possible to filter or modify the Generator, I thought it was easier to simply kick off the Generator with Byte values, because they are never negative.
Second, Gen.map int
converts the initial Gen<byte> to Gen<int> by invoking F#'s built-in int
conversion function.
Third, Gen.four
is a built-in FsCheck Generator Combinator that converts a Generator into a Generator of four-element tuples; in this case it converts Get<int> to Gen<int * int * int * int>: a Generator of a four-integer tuple.
Fourth, Gen.map (fun (ma, mi, bu, re) -> Version(ma, mi, bu, re))
converts Gen<int * int * int * int> to Gen<Version> by another application of Gen.map. The function supplied to Gen.map takes the four-element tuple of integers and invokes the Version constructor with the major, minor, build, and revision integer values.
Finally, Arb.fromGen
converts Gen<Version> to Arbitrary<Version>, which is what the member must return.
To register the Generators custom class with FsCheck, I'm currently doing this:
do Arb.register<Generators>() |> ignore
You can see this entire code in context here.