Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When implementing property-based testing, when should I use an input generator over a precondition expression?

When implementing property-based testing, when should I use an input generator over a precondition expression?

Are there performance considerations when selecting a particular option?

Internally, does one method inevitably use the other?

I would think that a precondition expression would take longer to execute in comparison to an input generator. Has anyone tested this?

Why would we need both?

like image 682
Scott Nimrod Avatar asked Mar 24 '16 00:03

Scott Nimrod


1 Answers

When you use a precondition expression (such as FsCheck's ==> operator), you're essentially throwing away data. Even if this only happens in one out of a hundred cases, you'd still be throwing away 1 input set for a normal property (because the default number of executions is 100, in FsCheck).

Throwing away one out of 100 is probably not a big deal.

Sometimes, however, you'd be throwing away a lot more data. If, for example, you want only positive numbers, you could write a precondition like x > 0, but since FsCheck generates negative numbers as well, you'd be throwing away 50 % of all values, after they have been generated. That's likely to make your tests run slower (but as always, when it comes to performance considerations: measure).

FsCheck comes with built-in generators for positive numbers for that very reason, but sometimes, you need more fine-grained control of the range of possible input values, as in this example.

If doing the FizzBuzz kata, for example, you may write your test for the FizzBuzz case like this:

[<Property(MaxFail = 2000)>]
let ``FizzBuzz.transform returns FizzBuzz`` (number : int) =
    number % 15 = 0 ==> lazy
    let actual = FizzBuzz.transform number
    let expected = "FizzBuzz"
    expected = actual

Notice the use of the MaxFail property. The reason you need it is because that precondition throws away 14 out of 15 generated candidates. By default, FsCheck will attempt 1000 candidates before it gives up, but if you throw away 14 out 15 candidates, on average you'll have only 67 values that match the precondition. Since FsCheck's default goal is to execute a property 100 times, it gives up.

As the MaxFail property implies, you can tweak the defaults. With 2000 candidates, you should expect 133 precondition matches on average.

It doesn't feel particularly efficient, though, so you can, alternatively use a custom generator:

[<Property(QuietOnSuccess = true)>]
let ``FizzBuzz.transform returns FizzBuzz`` () =
    let fiveAndThrees =
        Arb.generate<int> |> Gen.map ((*) (3 * 5)) |> Arb.fromGen
    Prop.forAll fiveAndThrees <| fun number ->

        let actual = FizzBuzz.transform number

        let expected = "FizzBuzz"
        expected = actual

This uses an ad-hoc in-line Arbitrary. This is more efficient because no data is thrown away.

My inclination is to use preconditions if it'd only throw away the occasional unmatching input. In most cases, I prefer custom generators.

like image 84
Mark Seemann Avatar answered Oct 14 '22 03:10

Mark Seemann