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

旧巷老猫 提交于 2019-11-30 23:13:06

问题


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.


回答1:


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.




回答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>


来源:https://stackoverflow.com/questions/21988046/how-does-one-generate-a-complex-object-in-fscheck

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