F# 在FsCheck中生成自定义数据
我有一个问题:F# 在FsCheck中生成自定义数据,f#,fscheck,F#,Fscheck,我有一个问题: 我有以下记录类型(我前面说过,有人告诉我,我的单一案例DU可能是一种滥杀滥伤,但我发现它们描述了该领域,因此是必要的,除非必须,否则我不会删除它们): 假设我已经定义了函数repeat:Int->('a->'a)->'a和decreaseQuality:Item->Item,我想编写一个FsCheck测试来检查不变量:任何非传奇风格的项目,在100天后,质量为0 我的问题是我不知道以下关于FsCheck的事情: 1.我如何定义一个自定义生成器来生成样式不是传奇的项目?相比之下,如
我有以下记录类型(我前面说过,有人告诉我,我的单一案例DU可能是一种滥杀滥伤,但我发现它们描述了该领域,因此是必要的,除非必须,否则我不会删除它们): 假设我已经定义了函数
repeat:Int->('a->'a)->'a
和decreaseQuality:Item->Item
,我想编写一个FsCheck
测试来检查不变量:任何非传奇风格的项目,在100天后,质量为0
我的问题是我不知道以下关于FsCheck
的事情:1.我如何定义一个自定义生成器来生成样式不是传奇的项目?相比之下,如何定义仅属于Legendary类型的项(以测试这两种类型) 我调查过:
let itemGenerator = Arb.generate<Item>
Gen.sample 80 5 itemGenerator
let itemGenerator=Arb.generate
Gen.sample 80 5项目生成器
但这会创建非常奇怪的项目,因为在示例中,大小
控制器80还控制字符串的名称
(由于字符串的)的长度,并且还会生成我的域无法接受的质量
和搁置寿命
值(即负值)因为它们都被定义为。。。整数的
,其大小也可控制。
(我也研究了……
的第1代,但结果也是一个哑弹)
谢谢 要始终生成有效的
质量
和搁置期
,您需要注册任意实例:
type Arbs =
static member Quality() =
Arb.Default.NonNegativeInt()
|> Arb.convert (fun (NonNegativeInt x) -> Quality x) (fun (Quality x) -> NonNegativeInt x)
static member ShelfLife() =
Arb.Default.NonNegativeInt()
|> Arb.convert (fun (NonNegativeInt x) -> ShelfLife x) (fun (ShelfLife x) -> NonNegativeInt x)
Arb.register<Arbs>()
类型Arbs=
静态成员质量()=
Arb.Default.NonNegativeInt()
|>Arb.convert(乐趣(非负性x)->质量x)(乐趣(质量x)->非负性x)
静态成员ShelfLife()=
Arb.Default.NonNegativeInt()
|>Arb.convert(乐趣(非负性x)->ShelfLife x)(乐趣(ShelfLife x)->非负性x)
仲裁寄存器()
对于您要检查的实际属性,这里有一个重新编写的代码,它将帮助您将其转换为FsCheck:如果样式不是传奇式的,那么100天后质量为0。代码:
let ``Non-legendary item breaks after 100 days`` (item: Item) =
(item.Style <> Legendary) ==>
let agedItem = item |> repeat 100 decreaseQuality
agedItem.Quality = Quality 0
让“非传奇物品在100天后破裂”(物品:物品)=
(item.Style传奇)==>
让agedItem=item |>重复100次降低质量
agedItem.Quality=质量0
一旦您知道如何最大限度地使用gen{}
计算表达式,您想要的大部分内容就会变得简单
首先,我将讨论如何生成一个非传奇的样式。您可以使用Gen.oneOf
,但在这种情况下,我认为使用Gen.elements
更简单,因为oneOf
需要使用一系列生成器,而elements
只需要一个项目列表,并从该列表中生成一个项目。因此,要生成一个非传奇的样式
,我将使用Gen.elements[Plain;Aged]
。(为了生成传奇风格的样式,我不需要使用生成器,只需将传奇指定给相应的记录字段,稍后会详细介绍。)
至于名称太长,为了将生成的字符串的大小限制为(比如)最多15个字符,我会使用:
let genString15 = Gen.sized (fun s -> Gen.resize (min s 15) Arb.generate<string>)
// Note: "min" is not a typo. We want either s or 15, whichever is SMALLER
Gen.sample 80 5 genString15
// Never produces any strings longer than 15 characters
现在,由于Quality
和ShelfLife
都不能是负数,我会使用PositiveInt
(其中也不允许0)或NonNegativeInt
(允许0)。这两种方法都没有在FsCheck文档中得到很好的记录,但它们的工作原理如下:
let x = Arb.generate<NonNegativeInt>
Gen.sample 80 5 x
// Produces [NonNegativeInt 79; NonNegativeInt 75; NonNegativeInt 0;
// NonNegativeInt 69; NonNegativeInt 16] which is hard to deal with
let y = Arb.generate<NonNegativeInt> |> Gen.map (fun (NonNegativeInt n) -> n)
Gen.sample 80 5 y
// Much better: [79; 75; 0; 69; 16]
type LegendaryItem = LegendaryItem of Item
type NonLegendaryItem = NonLegendaryItem of Item
最后,让我们用gen{}
CE以一种优雅的方式将这一切联系起来:
let genNonLegendaryItem = gen {
let! name = genString15 |> Gen.map Name
let! quality = genNonNegativeOf Quality
let! shelfLife = genNonNegativeOf Days
let! style = Gen.elements [Plain; Aged]
return {
Name = name
Quality = quality
ShelfLife = shelfLife
Style = style
}
}
let genLegendaryItem =
// This is the simplest way to avoid code duplication
genNonLegendaryItem
|> Gen.map (fun item -> { item with Style = Legendary })
一旦你做到了,要在你的测试中真正使用它,你需要注册发电机,正如塔米尔在他的回答中提到的。我可能会在这里使用单案例DU,以便测试易于编写,如下所示:
let x = Arb.generate<NonNegativeInt>
Gen.sample 80 5 x
// Produces [NonNegativeInt 79; NonNegativeInt 75; NonNegativeInt 0;
// NonNegativeInt 69; NonNegativeInt 16] which is hard to deal with
let y = Arb.generate<NonNegativeInt> |> Gen.map (fun (NonNegativeInt n) -> n)
Gen.sample 80 5 y
// Much better: [79; 75; 0; 69; 16]
type LegendaryItem = LegendaryItem of Item
type NonLegendaryItem = NonLegendaryItem of Item
然后将genLegendaryItem
和genNonLegendaryItem
生成器注册为通过Gen.map
生成(非)LegendaryItem
类型。然后您的测试用例将如下所示(我将在这里使用我的示例):
[]
让测试=
测试列表“项目到期”[
testProperty“非传奇项目在100天后过期”
让ItemAfter100天=item |>重复100次降低质量
ItemAfter100天。质量=质量0
testProperty“传奇项目永不过期”
让ItemAfter100天=item |>重复100次降低质量
项目100天后。质量>质量0
]
请注意,使用这种方法,您基本上必须自己编写收缩器,而使用Tarmil建议的Arb.convert
,则可以“免费”获得收缩器。不要低估收缩器的价值,但是如果你发现没有收缩器你也能活下去,我喜欢gen{}
计算表达式的美好、干净的本质,以及读取结果代码的容易程度。在解决了我在理解FsCheck
用法方面遇到的所有问题之后(就目前而言,我确信我将来会有更多的问题),以及我的整个解决方案
显然,测试代码位于(我认为名称恰当的)GildedRoseTests
文件夹中
我使用了上面rmunn建议的gen
计算表达式方法,但我做了一个不同的实验,使用Tarmil的Arb
方法也同样有效(而且你可以“免费”收缩)。(我之前的一条评论似乎已经被删除了…)感谢您向我展示如何正确使用Arb
类,特别是与我的用例相关的类。也感谢您重新编写我的不变量,使其在测试时有意义。非常有用的东西!谢谢!@O.F.BTW,这是教我如何使用的资源
type LegendaryItem = LegendaryItem of Item
type NonLegendaryItem = NonLegendaryItem of Item
[<Tests>]
let tests =
testList "Item expiration" [
testProperty "Non-legendary items expire after 100 days" <| fun (NonLegendaryItem item) ->
let itemAfter100Days = item |> repeat 100 decreaseQuality
itemAfter100Days.Quality = Quality 0
testProperty "Legendary items never expire" <| fun (LegendaryItem item) ->
let itemAfter100Days = item |> repeat 100 decreaseQuality
itemAfter100Days.Quality > Quality 0
]