我想创建一个FsCheck Generator来生成“复杂”对象的实例。复杂,我指的是C#中的现有类,它具有许多子属性和集合。这些属性和集合又需要为它们生成数据。
想象一下,这个类被命名为Menu
,其中包含子集合Dishes
和Drinks
(我正在这样做,所以忽略了糟糕的设计)。我想做以下事情:
Dishes
和可变数量的Drinks
。Dish
和Drink
个实例以填充其属性。Menu
实例上设置一些其他基本属性。 如何为这种类型的实例编写生成器?这是一个坏主意吗? (我是基于财产的测试的新手)。我已阅读过文档,但到目前为止显然未能将其全部内化。
有一个很好的example for generating a record,但这实际上只生成3个相同类型float
的值。
答案 0 :(得分:8)
这不是一个坏主意 - 实际上,这是你能够做到这一点的重点。 FsCheck的发电机是完全合成的。
首先请注意,如果你有不可变的对象,其构造函数采用原始类型,就像你的Drink和Dish一样,FsCheck可以生成这些开箱即用(使用反射)
let drinkArb = Arb.from<Drink>
let dishArb = Arb.from<Dish>
应该给你一个任意实例,它是一个生成器(生成一个随机的Drink实例)和一个收缩器(取一个Drink实例并使它'更小' - 这有助于调试,尤其是复合结构,你得到的地方如果您的测试失败,那么这是一个小例子。)
虽然这很快就会崩溃 - 在你的例子中,你可能不想要饮料数量或菜肴数量的负整数。上面的代码会生成负数。有时这很容易解决,如果你的类型实际上只是使用Arb.convert的某种类型的包装器,例如。
let drinksArb = Arb.Default.PositiveInt() |> Arb.convert (fun positive -> new Drinks(positive) (fun drinks -> drinks.Amount)
你需要提供往返Arb.convert和presto的转换,这是维护你的不变量的Drinks的新任意实例。当然,其他不变量可能不那么容易维护。
之后,从这两件产品同时生成发电机和收缩器变得有点困难。始终从发电机开始,然后在需要时(如果)需要时收缩机。 @ simonhdickson的例子看起来很合理。如果你有上面的任意实例,你可以通过调用.Generator来获取它们的生成器。
let drinksGen = drinksArb.Generator
一旦你有零件生成器(饮料和碟子),你可以确实将它们组合在一起,如@simonhdickson所建议:
let menuGenerator =
Gen.map3 (fun a b c -> Menu(a,b,c)) (Gen.listOf dishGenerator) (Gen.listOf drinkGenerator) (Arb.generate<int>)
分而治之!总的来看看Gen上的intellisense让你得到一些关于如何组合生成器的想法。
答案 1 :(得分:2)
可能有更好的方式来描述这个,但我认为这可能会做你想到的。 Drink
/ Dish
类型中的每一种都可以使用与menuGenerator
相同的样式进行更多参数
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>