Most of what you want becomes easy once you know how to use the gen { }
computation expression to maximum effect.
First, I'll tackle how to generate a Style
that isn't Legendary. You could use Gen.oneOf
, but in this case I think it's simpler to use Gen.elements
, since oneOf
takes a sequence of generators to use, but elements
just takes a list of items and generates one item from that list. So to generate a Style
that isn't Legendary, I'd use Gen.elements [Plain; Aged]
. (And to generate a Style
that is Legendary, I'd just not use a generator and simply assign Legendary to the appropriate record field, but more on that later.)
As for names being too long, to limit the size of the strings produced to, say, a maximum of 15 characters, I'd use:
let genString15 = Gen.sized (fun s -> Gen.resize (min s 15) Arb.generate<string>)
// Note: "min" is not a typo. We want either s or 15, whichever is SMALLER
Gen.sample 80 5 genString15
// Never produces any strings longer than 15 characters
But this can still generate null
strings, so I'd probably use this for my final version:
let genString15 =
Gen.sized (fun s -> Gen.resize (min s 15) Arb.generate<NonNull<string>>)
|> Gen.map (fun (NonNull x) -> x) // Unwrap
Gen.sample 80 5 genString15
// Never produces any strings longer than 15 characters, AND never produces null
Now, since Quality
and ShelfLife
both can't be negative, I'd use either PositiveInt
(where 0 isn't allowed either) or NonNegativeInt
(which allows 0). Neither one is well documented in the FsCheck documentation, but they work like this:
let x = Arb.generate<NonNegativeInt>
Gen.sample 80 5 x
// Produces [NonNegativeInt 79; NonNegativeInt 75; NonNegativeInt 0;
// NonNegativeInt 69; NonNegativeInt 16] which is hard to deal with
let y = Arb.generate<NonNegativeInt> |> Gen.map (fun (NonNegativeInt n) -> n)
Gen.sample 80 5 y
// Much better: [79; 75; 0; 69; 16]
To avoid duplicating code between generators for Quality
and Days
, I'd write something like the following:
let genNonNegativeOf (f : int -> 'a) = gen {
let! (NonNegativeInt n) = Arb.generate<NonNegativeInt>
return (f n)
}
Gen.sample 80 5 (genNonNegativeOf Quality)
// Produces: [Quality 79; Quality 35; Quality 2; Quality 42; Quality 73]
Gen.sample 80 5 (genNonNegativeOf Days)
// Produces: [Days 60; Days 27; Days 50; Days 22; Days 23]
And finally, let's tie that all together in a nice, elegant fashion with a gen { }
CE:
let genNonLegendaryItem = gen {
let! name = genString15 |> Gen.map Name
let! quality = genNonNegativeOf Quality
let! shelfLife = genNonNegativeOf Days
let! style = Gen.elements [Plain; Aged]
return {
Name = name
Quality = quality
ShelfLife = shelfLife
Style = style
}
}
let genLegendaryItem =
// This is the simplest way to avoid code duplication
genNonLegendaryItem
|> Gen.map (fun item -> { item with Style = Legendary })
Then once you've done that, to actually use this in your tests, you'll need to register the generators, as Tarmil mentioned in his answer. I'd probably use single-case DUs here so that the tests are easy to write, like this:
type LegendaryItem = LegendaryItem of Item
type NonLegendaryItem = NonLegendaryItem of Item
Then you'd register the genLegendaryItem
and genNonLegendaryItem
generators as producing (Non)LegendaryItem
types by passing them through a Gen.map
. And then your test cases would look like this (I'll use Expecto for my example here):
[<Tests>]
let tests =
testList "Item expiration" [
testProperty "Non-legendary items expire after 100 days" <| fun (NonLegendaryItem item) ->
let itemAfter100Days = item |> repeat 100 decreaseQuality
itemAfter100Days.Quality = Quality 0
testProperty "Legendary items never expire" <| fun (LegendaryItem item) ->
let itemAfter100Days = item |> repeat 100 decreaseQuality
itemAfter100Days.Quality > Quality 0
]
Note that with this approach, you'd basically have to write the shrinkers yourself, whereas using Arb.convert
as Tarmil suggested would get you shrinkers "for free". Don't underestimate the value of shrinkers, but if you find you can live without them, I like the nice, clean nature of the gen { }
computation expression, and how easy it is to read the resulting code.