2

I am currently writing a property based test to test a rate calculation function in f# with 4 float parameters, and all the parameters have specific conditions for them to be valid (for example, a > 0.0 && a < 1.0, and b > a). I do have a function checking if these conditions are met and returning a bool. My question is, in my test code using [Property>] in FsCheck.Xunit, how do I limit the generator to test the codes using only values meeting my specific conditions for the parameters?

wolfffff
  • 23
  • 2

2 Answers2

3

If you are using FsCheck then you can use the Gen.filter function and the Gen.map function.

Lets say you have this function funToBeTested that you are testing, that requires that a < b:

let funToBeTested a b = if a < b then a + b else failwith "a should be less than b" 

And you are testing the property that funToBeTested be proportional to the inputs:

let propertyTested a b = funToBeTested a b / 2. = funToBeTested (a / 2.) (b / 2.)

You also have a predicate that checks the condition requirements for a & b:

let predicate a b = a > 0.0 && a < 1.0 && b > a

We start by generating float numbers using Gen.choose and Gen.map, this way already produces values only from 0.0 to 1.0:

let genFloatFrom0To1 = Gen.choose (0, 10000) |> Gen.map (fun i -> float i / 10000.0 )

Then we generate two floats from 0 to 1 and filter them using the predicate function above

let genAB            = Gen.two genFloatFrom0To1 |> Gen.filter (fun (a,b) -> predicate a b )

Now we need to create a new type TestData for using those values:

type TestData = TestData of float * float

and we map the resulting value to TestData

let genTest = genAB |> Gen.map TestData

Next we need to register genTest as the generator for TestData for that we create a new class with a static member of type Arbitrary<TestData>:

type MyGenerators =
    static member TestData : Arbitrary<TestData> = genTest |> Arb.fromGen

Arb.register<MyGenerators>() |> ignore

finally we test the property using TestData as the input:

Check.Quick (fun (TestData(a, b)) -> propertyTested a b )

UPDATE:

An easy way to compose different generators is using gen Computation Expression:

type TestData = {
    a : float
    b : float 
    c : float 
    n : int
}

let genTest = gen {
    let! a = genFloatFrom0To1
    let! b = genFloatFrom0To1
    let! c = genFloatFrom0To1
    let! n = Gen.choose(0, 30)
    return {
        a = a
        b = b
        c = c
        n = n
    }
}

type MyGenerator =
    static member TestData : Arbitrary<TestData> = genTest |> Arb.fromGen

Arb.register<MyGenerator>() |> ignore


let ``Test rate Calc`` a b c n =                               
    let r = rCalc a b c
    (float) r >= 0.0 && (float) r <= 1.0    


Check.Quick (fun (testData:TestData) -> 
    ``Test rate Calc`` 
        testData.a 
        testData.b 
        testData.c 
        testData.n)         
AMieres
  • 4,944
  • 1
  • 14
  • 19
  • Hi, I followed this logic in my code but instead of Check.Quick I had to use [|])>] because I kept getting "no tests can be found" when I run with Check.Quick. However, when I run the test it kept failing because the numbers generated in the test are still out of bounds (i often was getting -infinity in one of the a,b). I suspect that it was because the [] clause did not function properly somehow. Do you know what might be happening? Thanks again – wolfffff Feb 22 '19 at 15:54
  • Your property function ``Test rate Calc`` needs to receive `TestData(a, b, c)` as a parameter, currently it receives `a b c`. – AMieres Feb 22 '19 at 19:51
  • It works! Thank you! If you don't mind me asking another question, if I want to append the TestData type with another int variable from Gen.choose(1,30), so testdata will be float* float * float*int, how do I modify genTest or genAB to map the new TestData type? – wolfffff Feb 22 '19 at 20:40
  • I added a way to do it at the end of the answer. I also changed `TestData` from DU to record, because it gets messy when there are too many fields in the DU. – AMieres Feb 22 '19 at 21:32
3

The answer by @AMieres is a great explanation of everything you need to solve this!

One minor addition is that using Gen.filter can be tricky if the predicate does not hold for a large number of elements that your generator produces, because then the generator needs to run for a long time until it finds sufficient number of valid elements.

In the example by @AMieres, it is fine, because the generator generates numbers in the right range already and so it only checks that the second one is larger, which will be the case for about half of the randomly generated pairs.

If you can write this so that you always generate valid values, then that's a bit better. My version for this particular case would be to use map to swap the numbers so that the smaller one is always first:

let genFloatFrom0To1 = Gen.choose (0, 10000) |> Gen.map (fun i -> float i / 10000.0 )
let genAB = Gen.two genFloatFrom0To1 |> Gen.map (fun (a, b) -> min a b, max a b)
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553