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
        ]