How does one generate a “complex” object in FsCheck?

烂漫一生 提交于 2019-12-01 03:27:11

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.

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>
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!