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?

2 Answers 2

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)         
Sign up to request clarification or add additional context in comments.

4 Comments

Hi, I followed this logic in my code but instead of Check.Quick I had to use [<Property (Arbitrary = [|typeof<MyGenerators>|])>] 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 [<Property (Arbitrary...)>] clause did not function properly somehow. Do you know what might be happening? Thanks again
Your property function Test rate Calc needs to receive TestData(a, b, c) as a parameter, currently it receives a b c.
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?
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.
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)

1 Comment

That is an elegant solution!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.