I'm trying to learn how to use FsCheck properly, and integrating it with Expecto at the moment. I can get property tests to run if I'm using the default FsCheck config, but when I try to use my own Generator, it causes a stack overflow exception.
Here is my generator
type NameGen() =
static member Name() =
Arb.generate<string * string>
|> Gen.where (fun (firstName, lastName) ->
firstName.Length > 0 && lastName.Length > 0
)
|> Gen.map (fun (first, last) -> sprintf "%s %s" first last)
|> Arb.fromGen
|> Arb.convert string id
And I'm trying to use it like this:
let config = { FsCheckConfig.defaultConfig with arbitrary = [typeof<NameGen>] }
let propertyTests input =
let output = toInitials input
output.EndsWith(".")
testPropertyWithConfig config "Must end with period" propertyTests
The exception is thrown before it even gets into the Gen.where
function
What am I doing wrong? Thanks
You are trying to use FsCheck's generator of strings to redefine how its generator of strings work, but when you do that, it'll recursively call itself until it runs out of stack space. It's a known issue: https://github.com/fscheck/FsCheck/issues/109
Does this alternative work?
type NameGen =
static member Name () =
Arb.Default.NonEmptyString().Generator
|> Gen.map (fun (NonEmptyString s) -> s)
|> Gen.two
|> Gen.map (fun (first, last) -> sprintf "%s %s" first last)
|> Arb.fromGen
You are defining a new generator for the type string, but within that you're using the generator for string * string
, which uses the generator for string
. FsCheck unfortunately seems to store generators in global mutable state (perhaps with good reason?) and I think this means that the generator keeps calling itself until stack overflow.
You could solve this by defining the generator for a custom wrapper type instead of a plain string (shown below).
The next problem you'll hit will be null reference exceptions. The initial generated string could be null
and you try to access the .Length
property. This can be resolved using the String.length
function instead, which returns 0
for null
.
With these changes your generator looks like this:
type Name = Name of string
type NameGen() =
static member Name() =
Arb.generate<string * string>
|> Gen.where (fun (firstName, lastName) ->
String.length firstName > 0 && String.length lastName > 0
)
|> Gen.map (fun (first, last) -> sprintf "%s %s" first last)
|> Arb.fromGen
|> Arb.convert Name (fun (Name n) -> n)
And your property needs a slight modification:
let propertyTests (Name input) =
let output = toInitials input
output.EndsWith(".")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With