10

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

Menu想象一下这个类是用子集合命名的,Dishes并且Drinks(我正在编造这个,所以忽略糟糕的设计)。我想做以下事情:

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

如何为这种类型的实例编写生成器?这是一个坏主意吗?(我是基于属性的测试的新手)。我已经阅读了文档,但到目前为止显然还没有将其内化。

有一个不错生成记录示例,但这实际上只生成了 3 个相同类型的值float

4

2 回答 2

7

这不是一个坏主意——事实上,这是你能够做到这一点的关键。FsCheck 的生成器是完全组合的。

首先请注意,如果您有其构造函数采用原始类型的不可变对象,例如您的 Drink 和 Dish 的样子,FsCheck 可以开箱即用地生成这些对象(使用反射)

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

应该给你一个 Arbitrary 实例,它是一个生成器(生成一个随机 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

一旦有了零件生成器(Drink 和 Dish),您确实可以按照@simonhdickson 的建议将它们组合在一起:

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

分而治之!总的来说,看看 Gen 上的 intellisense 为您提供了一些关于如何组成生成器的想法。

于 2014-02-25T16:08:07.273 回答
2

可能有更好的描述方式,但我认为这可能符合您的想法。每个Drink/类型都Dish可以使用与 dos 相同的样式来获取更多参数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>
于 2014-02-24T13:42:45.347 回答