How do I implement multiple argument generation using FsCheck?

拟墨画扇 提交于 2019-12-23 22:40:03

问题


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

回答1:


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.



来源:https://stackoverflow.com/questions/38839721/how-do-i-implement-multiple-argument-generation-using-fscheck

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!