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
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)

- 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 [ – wolfffff Feb 22 '19 at 15:54] 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`. – 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
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)

- 240,744
- 19
- 378
- 553