如何在FsCheck中生成“复杂”对象?

时间:2014-02-24 12:39:24

标签: f# fscheck

我想创建一个FsCheck Generator来生成“复杂”对象的实例。复杂,我指的是C#中的现有类,它具有许多子属性和集合。这些属性和集合又需要为它们生成数据。

想象一下,这个类被命名为Menu,其中包含子集合DishesDrinks(我正在这样做,所以忽略了糟糕的设计)。我想做以下事情:

  • 生成可变数量的Dishes和可变数量的Drinks
  • 使用FsCheck API生成DishDrink个实例以填充其属性。
  • 使用FsCheck API在Menu实例上设置一些其他基本属性。

如何为这种类型的实例编写生成器?这是一个坏主意吗? (我是基于财产的测试的新手)。我已阅读过文档,但到目前为止显然未能将其全部内化。

有一个很好的example for generating a record,但这实际上只生成3个相同类型float的值。

2 个答案:

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