How do I implement multiple argument generation using FsCheck?
I implemented the following to support multiple argument generation:
// Setup
let pieces = Arb.generate<Piece> |> Gen.filter (isKing >> not)
|> Arb.fromGen
let positionsList = Arb.generate<Space list> |> Arb.fromGen
I then used these arguments to test the behavior of a function that's responsible for generating move options for a given checker:
// Test
Prop.forAll pieces <| fun piece ->
Prop.forAll positionsList <| fun positionsItem ->
positionsItem |> optionsFor piece
|> List.length <= 2
Is nesting Prop.forAll expressions the right technique when managing multiple generated argument types?
Is there an alternative method for generating multiple arguments for a function under test?
Here's the entire function:
open FsCheck
open FsCheck.Xunit
[<Property(QuietOnSuccess = true)>]
let ``options for soldier can never exceed 2`` () =
// Setup
let pieces = Arb.generate<Piece> |> Gen.filter (isKing >> not)
|> Arb.fromGen
let positionsList = Arb.generate<Space list> |> Arb.fromGen
// Test
Prop.forAll pieces <| fun piece ->
Prop.forAll positionsList <| fun positionsItem ->
positionsItem |> optionsFor piece
|> List.length <= 2
UPDATE
Here's the solution to my question derived from Mark's answer:
[<Property(QuietOnSuccess = true, MaxTest=100)>]
let ``options for soldier can never exceed 2`` () =
// Setup
let pieceGen = Arb.generate<Piece> |> Gen.filter (isKing >> not)
let positionsGen = Arb.generate<Space list>
// Test
(pieceGen , positionsGen) ||> Gen.map2 (fun x y -> x,y)
|> Arb.fromGen
|> Prop.forAll <| fun (piece , positions) ->
positions |> optionsFor piece
|> List.length <= 2
As a general observation, Arbitrary
values are difficult to compose, whereas Gen
values are easy. For that reason, I tend to define my FsCheck building blocks in terms of Gen<'a>
instead of Arbitrary<'a>
.
With Gen
values, you can compose multiple arguments using Gen.map2
, Gen.map3
, etcetera, or you can use the gen
computation expression.
Gen building blocks
In the OP example, instead of defining pieces
and positionsList
as Arbitrary
, define them as Gen
values:
let genPieces = Arb.generate<Piece> |> Gen.filter (isKing >> not)
let genPositionsList = Arb.generate<Space list>
These are 'building blocks' of the types Gen<Piece>
and Gen<Space list>
, respectively.
Notice that I named them genPieces
instead of simply pieces
, and so on. This prevents name collisions later on (see below). (Also, I'm not sure about the use of the plural s in pieces
, because genPieces
only generates a single Piece
value, but I since I don't know your entire domain, I decided to leave that as is.)
If you need only one of them, you can convert it into an Arbitrary
using Arb.fromGen
.
If you need to compose them, you can use either one of the map functions, or computation expressions, as shown below. This will give you a Gen
of tuples, and you can then use Arb.fromGen
to convert that into an Arbitrary
.
Compose using map2
If you need to compose pieces
and positionsList
into an argument list, you can use Gen.map2
:
Gen.map2 (fun x y -> x, y) genPieces genPositionList
|> Arb.fromGen
|> Prop.forAll <| fun (pieces, positionList) ->
// test goes here...
Gen.map2 (fun x y -> x, y)
returns a two-element tuple (a pair) of values, which you can destructure into (pieces, positionList)
in the anonymous function.
This example should also make it clear why genPieces
and genPositionList
are better names for the Gen
values: they leave room to use the 'naked' names pieces
and positionList
for the generated values passed to the test body.
Compose using computation expression
Another alternative that I sometimes prefer for more complex combinations is to use the gen
computation expression.
The above example could also be written like this:
gen {
let! pieces = genPieces
let! positionList = genPositionList
return pieces, positionList }
|> Arb.fromGen
|> Prop.forAll <| fun (pieces, positionList) ->
// test goes here...
The initial gen
expression also returns a pair, so it's equivalent to composition with Gen.map2
.
You can use the option you find most readable.
You can see more examples of non-trivial Gen
combinations in my article Roman numerals via property-based TDD.
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