How does one generate a "complex" object in FsCheck?
Asked Answered
G

2

11

I'd like to create an FsCheck Generator to generate instances of a "complex" object. By complex, I mean an existing class in C# that has a number of child properties and collections. These properties and collections in turn need to have data generated for them.

Imagine this class was named Menu with child collections Dishes and Drinks (I'm making this up so ignore the crappy design). I want to do the following:

  • Generate a variable number of Dishes and a variable number of Drinks.
  • Generate the Dish and Drink instances using the FsCheck API to populate their properties.
  • Set some other primitive properties on the Menu instance using the FsCheck API.

How does one go about writing a generator for this type of instance? Is this a bad idea? (I'm new to property based testing). I have read the docs, but have clearly failed to internalise it all so far.

There is a nice example for generating a record, but this is really only generating 3 values of the same type float.

Gluttonize answered 24/2, 2014 at 12:39 Comment(3)
examples: github.com/fsharp/FsCheck/blob/master/FsCheck/Arbitrary.fs#L347 github.com/fsharp/FsCheck/blob/master/FsCheck/Arbitrary.fs#L555Chloromycetin
@MauricioScheffer both links are dead =(Equilibrate
As @MauricioScheffer's links are dead, here is a link to the DateTime generator/shrinker code github.com/fscheck/FsCheck/blob/… (guessing this was probably one of the previous examples).Gluttonize
I
8

This is not a bad idea - in fact it's the whole point that you are able to do this. FsCheck's generators are fully compositional.

Note first that if you have immutable objects whose constructors take primitive types, like your Drink and Dish looks like, FsCheck can generate these out of the box (using reflection)

let drinkArb = Arb.from<Drink>
let dishArb = Arb.from<Dish>

should give you an Arbitrary instance, which is a generator (generates a random Drink instance) and a shrinker (takes a Drink instance and makes it 'smaller' - this helps with debugging, esp. for composite structures, where you get a small counter-example if your test fails).

This breaks down fairly quickly though - in your example you probably don't want negative integers for the number of drinks or the number of dishes. The above code will generate negative numbers though. Sometimes this is easy to fix if your type is really just a wrapper of some sort around another type, using Arb.convert, e.g.

let drinksArb = Arb.Default.PositiveInt() |> Arb.convert (fun positive -> new Drinks(positive) (fun drinks -> drinks.Amount)

You need to provide to and from conversions to Arb.convert and presto, new arbitrary instance for Drinks that maintains your invariant. Other invariants may not be so easy to maintain of course.

After that it becomes a bit harder to generate a generator and a shrinker at the same time from those two pieces. Always start with the generator, then shrinker comes later if (when) you need it. @simonhdickson's example looks reasonable. If you have the arbitrary instances above, you can get at their generator by calling .Generator.

let drinksGen = drinksArb.Generator

Once you have the parts generators (Drink and Dish), you can indeed compose them together as @simonhdickson proposes:

let menuGenerator =
    Gen.map3 (fun a b c -> Menu(a,b,c)) (Gen.listOf dishGenerator) (Gen.listOf drinkGenerator) (Arb.generate<int>)

Divide and conquer! Overall have a look at what intellisense on Gen gives you to get some ideas of how to compose generators.

Irresoluble answered 25/2, 2014 at 16:8 Comment(2)
Can you provide an example in C#?Consalve
for a simple c# example see: #35203790Evidently
V
2

There might be a better way of describing this, but I think this might do what you're thinking of. Each of the Drink/Dish types could take further parameters using the same kind of style as the menuGenerator does

type Drink() =
    member m.X = 1

type Dish() =
    member m.Y = 2

type Menu(dishes:Dish list, drinks:Drink list, total:int) =
    member m.Dishes = dishes
    member m.Drinks = drinks
    member m.Total = total

let drinkGenerator = Arb.generate<unit> |> Gen.map (fun () -> Drink())
let dishGenerator = Arb.generate<unit> |> Gen.map (fun () -> Dish())
let menuGenerator =
    Gen.map3 (fun a b c -> Menu(a,b,c)) <| Gen.listOf dishGenerator <| Gen.listOf drinkGenerator <| Arb.generate<int>
Violist answered 24/2, 2014 at 13:42 Comment(2)
generate<unit>, map3... unnecessarily complicated IMHO.Chloromycetin
True, the link example you posted is far superior to this.Violist

© 2022 - 2024 — McMap. All rights reserved.