使用FsCheck

时间:2016-03-10 17:34:06

标签: f# fscheck

问题

在F#中,我使用FsCheck生成一个对象(我在Xunit测试中使用它,但我可以完全在Xunit之外重新创建,所以我认为我们可以忘记Xunit)。在FSI中运行20代,

  • 50%的时间,这一代成功运行。
  • 25%的时间,一代人抛出:

    System.ArgumentException: The input must be non-negative.
    Parameter name: index
    >    at Microsoft.FSharp.Collections.SeqModule.Item[T](Int32 index, IEnumerable`1 source)
       at FsCheck.GenBuilder.bind@62.Invoke(Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at FsCheck.Gen.go@290-1[b](FSharpList`1 gs, FSharpList`1 acc, Int32 size, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 295
       at FsCheck.Gen.SequenceToList@297.Invoke(Int32 n, StdGen r) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 297
       at FsCheck.GenBuilder.bind@62.Invoke(Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at FsCheck.Gen.sample@155[a](Int32 size, Gen`1 gn, Int32 i, StdGen seed, FSharpList`1 samples) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 157
       at FsCheck.Gen.Sample[a](Int32 size, Int32 n, Gen`1 gn) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 155
       at <StartupCode$FSI_0026>.$FSI_0026.main@() in C:\projects\Alberta\Core\TestFunc\Script.fsx:line 57
    Stopped due to error
    
  • 25%的时间,一代人抛出:

    System.ArgumentException: The input sequence has an insufficient number of elements.
    Parameter name: index
    >    at Microsoft.FSharp.Collections.IEnumerator.nth[T](Int32 index, IEnumerator`1 e)
       at Microsoft.FSharp.Collections.SeqModule.Item[T](Int32 index, IEnumerable`1 source)
       at FsCheck.GenBuilder.bind@62.Invoke(Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at FsCheck.Gen.go@290-1[b](FSharpList`1 gs, FSharpList`1 acc, Int32 size, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 295
       at FsCheck.Gen.SequenceToList@297.Invoke(Int32 n, StdGen r) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 297
       at FsCheck.GenBuilder.bind@62.Invoke(Int32 n, StdGen r0) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 63
       at FsCheck.Gen.sample@155[a](Int32 size, Gen`1 gn, Int32 i, StdGen seed, FSharpList`1 samples) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 157
       at FsCheck.Gen.Sample[a](Int32 size, Int32 n, Gen`1 gn) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Gen.fs:line 155
       at <StartupCode$FSI_0025>.$FSI_0025.main@() in C:\projects\Alberta\Core\TestFunc\Script.fsx:line 57
    Stopped due to error
    

情况

对象如下:

type Event =
| InitEvent of string
| RefEvent of string
type Stream = Event seq

对象必须遵循这些规则才有效:

  1. 所有InitEvents必须在所有RefEvents
  2. 之前
  3. 所有InitEvents字符串必须是唯一的
  4. 所有RefEvent名称必须具有较早的相应InitEvent
  5. 但如果某些InitEvents后来没有相应的RefEvents
  6. ,那就没关系
  7. 但如果多个RefEvent具有相同的名称,那就没关系
  8. 工作解决方法

    如果我让生成器调用一个返回有效对象并执行Gen.constant(函数)的函数,我从不遇到异常,但这不是FsCheck的运行方式! :)

    /// <summary>
    /// This is a non-generator equivalent which is 100% reliable
    /// </summary>
    let randomStream size =
       // valid names for a sample
       let names = Gen.sample size size Arb.generate<string> |> List.distinct
       // init events
       let initEvents = names |> List.map( fun name -> name |> InitEvent )
       // reference events
       let createRefEvent name = name |> RefEvent
       let genRefEvent = createRefEvent <!> Gen.elements names
       let refEvents = Gen.sample size size genRefEvent
       // combine
       Seq.append initEvents refEvents
    
    
    type MyGenerators =
       static member Stream() = {
          new Arbitrary<Stream>() with
             override x.Generator = Gen.sized( fun size -> Gen.constant (randomStream size) )
       }
    
    // repeatedly running the following two lines ALWAYS works
    Arb.register<MyGenerators>()
    let foo = Gen.sample 10 10 Arb.generate<Stream>
    

    破碎正确的方式?

    我似乎无法完全避免生成常量(需要在InitEvents之外存储名称列表,以便RefEvent生成可以获取它们,但我可以更好地了解FsCheck生成器的工作方式:

    type MyGenerators =
       static member Stream() = {
          new Arbitrary<Stream>() with
             override x.Generator = Gen.sized( fun size ->
                // valid names for a sample
                let names = Gen.sample size size Arb.generate<string> |> List.distinct
                // generate inits
                let genInits = Gen.constant (names |> List.map InitEvent) |> Gen.map List.toSeq
                // generate refs
                let makeRef name = name |> RefEvent
                let genName = Gen.elements names
                let genRef = makeRef <!> genName
                Seq.append <!> genInits <*> ( genRef |> Gen.listOf )
             )
       }
    
    // repeatedly running the following two lines causes the inconsistent errors
    // If I don't re-register my generator, I always get the same samples.
    // Is this because FsCheck is trying to be deterministic?
    Arb.register<MyGenerators>()
    let foo = Gen.sample 10 10 Arb.generate<Stream>
    

    我已经查看的内容

    • 很抱歉,忘了在原始问题中提及我已尝试在交互式调试,并且由于行为不一致,它有点不对难以追查。然而,当异常命中时,它似乎在我的生成器代码的结尾和要求生成的样本之间 - 当FsCheck正在进行生成时,它似乎正在尝试处理格式错误的序列。我进一步假设这是因为我对发电机编码不正确。
    • IndexOutOfRangeException using FsCheck表明可能存在类似情况。我尝试通过 Resharper测试跑步者以及 Xunit的控制台测试跑步者在上述简化所基于的实际测试中运行我的Xunit测试。两个跑步者表现出相同的行为,因此问题出在其他地方。
    • 其他&#34;我如何生成...&#34;诸如In FsCheck, how to generate a test record with non-negative fields?How does one generate a "complex" object in FsCheck?之类的问题涉及复杂程度较低的对象的创建。第一个是获得我所拥有的代码的一个很好的帮助,第二个提供了 Arb.convert 的急需的例子,但是如果我&#39,Arb.convert没有意义。 ; m转换为&#34;常数&#34;随机生成的名称列表。这一切似乎都回到了这一点 - 需要制作随机名称,然后将其拉出来制作一套完整的InitEvents,以及一些RefEvents序列,这两个序列都引用了&#34;常量&#34;列表,并不匹配我遇到过的任何内容。
    • 我已经查看了我能找到的大多数FsCheck生成器示例,包括FsCheck中包含的示例:https://github.com/fscheck/FsCheck/blob/master/examples/FsCheck.Examples/Examples.fs这些也不处理需要内部一致性的对象,并且似乎不适用于这种情况,即使它们总体上有所帮助。
    • 也许这意味着我从一个无益的角度来接近对象的生成。如果生成符合上述规则的对象的方法不同,我可以切换到它。
    • 进一步退出问题,我已经看到其他SO帖子大致说&#34;如果您的对象有这样的限制,那么当您收到无效对象时会发生什么?也许你需要重新考虑这个对象的使用方式,以便更好地处理无效的情况。&#34;例如,如果我能够在RefEvent中即时初始化一个前所未见的名称,那么首先给出InitEvent的全部需求将会消失 - 问题优雅地简化为一些随机的RefEvents序列名称。我对这种解决方案持开放态度,但这需要一些返工 - 从长远来看,这可能是值得的。与此同时,问题仍然存在,您如何使用FsCheck可靠地生成遵循上述规则的复杂对象?

    谢谢!

    编辑(S):尝试解决

    • Mark Seemann的答案中的代码有效,但产生的对象与我想要的略有不同(我的目标规则中我不清楚 - 现在有希望澄清)。将他的工作代码放在我的生成器中:

      type MyGenerators =
         static member Stream() = {
            new Arbitrary<Stream>() with
               override x.Generator =
                  gen {
                     let! uniqueStrings = Arb.Default.Set<string>().Generator
                     let initEvents = uniqueStrings |> Seq.map InitEvent
      
                     let! sortValues =
                        Arb.Default.Int32()
                        |> Arb.toGen
                        |> Gen.listOfLength uniqueStrings.Count
                     let refEvents =
                        Seq.zip uniqueStrings sortValues
                        |> Seq.sortBy snd
                        |> Seq.map fst
                        |> Seq.map RefEvent
      
                     return Seq.append initEvents refEvents
                  }
          }
      

      这会产生一个对象,其中每个InitEvent都有一个匹配的RefEvent,每个InitEvent只有一个RefEvent。我试图调整代码,以便为每个名称获取多个RefEvent,并且并非所有名称都需要具有RefEvent。例如:Init foo,Init bar,Ref foo,Ref foo完全有效。试着通过以下方式进行调整:

      type MyGenerators =
         static member Stream() = {
            new Arbitrary<Stream>() with
               override x.Generator =
                  gen {
                     let! uniqueStrings = Arb.Default.Set<string>().Generator
                     let initEvents = uniqueStrings |> Seq.map InitEvent
      
                     // changed section starts
                     let makeRef name = name |> RefEvent
                     let genRef = makeRef <!> Gen.elements uniqueStrings
                     return! Seq.append initEvents <!> ( genRef |> Gen.listOf )
                     // changed section ends
                  }
         }
      

      修改后的代码仍然表现出不一致的行为。有趣的是,在20次样本运行中,只有3次运行(从10次开始),而元素数量不足被抛出8次而输入必须是非负的是抛出了9次 - 这些变化使得边缘案件被击中的可能性增加了两倍多。我们现在归结为错误的一小部分代码。

    • Mark很快回复了另一个版本以解决变更的要求:

      type MyGenerators =
         static member Stream() = {
            new Arbitrary<Stream>() with
               override x.Generator =
                  gen {
                     let! uniqueStrings = Arb.Default.NonEmptySet<string>().Generator
                     let initEvents = uniqueStrings.Get |> Seq.map InitEvent
      
                     let! refEvents =
                        uniqueStrings.Get |> Seq.map RefEvent |> Gen.elements |> Gen.listOf
      
                     return Seq.append initEvents refEvents
                  }
         }
      

      这允许某些名称没有RefEvent。

    最终代码 一个非常小的调整得到它,以便可能发生重复的RefEvents:

    type MyGenerators =
       static member Stream() = {
          new Arbitrary<Stream>() with
             override x.Generator =
                gen {
                   let! uniqueStrings = Arb.Default.NonEmptySet<string>().Generator
                   let initEvents = uniqueStrings.Get |> Seq.map InitEvent
    
                   let! refEvents =
                      //uniqueStrings.Get |> Seq.map RefEvent |> Gen.elements |> Gen.listOf
                      Gen.elements uniqueStrings.Get |> Gen.map RefEvent |> Gen.listOf
    
                   return Seq.append initEvents refEvents
                }
       }
    

    非常感谢Mark Seemann!

1 个答案:

答案 0 :(得分:6)

这是满足要求的一种方法:

open FsCheck

let streamGen = gen {
    let! uniqueStrings = Arb.Default.Set<string>().Generator
    let initEvents = uniqueStrings |> Seq.map InitEvent

    let! sortValues =
        Arb.Default.Int32()
        |> Arb.toGen
        |> Gen.listOfLength uniqueStrings.Count
    let refEvents =
        Seq.zip uniqueStrings sortValues
        |> Seq.sortBy snd
        |> Seq.map fst
        |> Seq.map RefEvent

    return Seq.append initEvents refEvents }

semi-official answer on how to generate unique strings是生成Set<string>。由于Set<'a>也会实现'a seq,因此您可以使用所有正常的Seq函数。

然后,生成InitEvent值是对唯一字符串的简单map操作。

由于每个RefEvent必须具有相应的InitEvent,因此您可以重复使用相同的唯一字符串,但您可能希望将RefEvent值设置为以不同的顺序显示。为此,您可以生成sortValues,这是一个随机int值列表。此列表与字符串集的长度相同。

此时,您有一个唯一字符串列表和一个随机整数列表。以下是一些说明这个概念的假值:

> let uniqueStrings = ["foo"; "bar"; "baz"];;
val uniqueStrings : string list = ["foo"; "bar"; "baz"]

> let sortValues = [42; 1337; 42];;    
val sortValues : int list = [42; 1337; 42]

您现在可以zip他们:

> List.zip uniqueStrings sortValues;;
val it : (string * int) list = [("foo", 42); ("bar", 1337); ("baz", 42)]

在第二个元素上对这样的序列进行排序会给你一个随机洗牌的列表,然后你可以只map到第一个元素:

> List.zip uniqueStrings sortValues |> List.sortBy snd |> List.map fst;;
val it : string list = ["foo"; "baz"; "bar"]

由于所有InitEvent值必须在RefEvent值之前,您现在可以将refEvents附加到initEvents,并返回此组合列表。

验证

您可以验证streamGen是否按预期工作:

open FsCheck.Xunit
open Swensen.Unquote

let isInitEvent = function InitEvent _ -> true | _ -> false
let isRefEvent =  function RefEvent  _ -> true | _ -> false

[<Property(MaxTest = 100000)>]
let ``All InitEvents must come before all RefEvents`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        test <@ s |> Seq.skipWhile isInitEvent |> Seq.forall isRefEvent @>

[<Property(MaxTest = 100000)>]
let ``All InitEvents strings must be unique`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s |> Seq.choose (function InitEvent s -> Some s | _ -> None)
        let distinctStrings = initEventStrings |> Seq.distinct

        distinctStrings |> Seq.length =! (initEventStrings |> Seq.length)

[<Property(MaxTest = 100000)>]
let ``All RefEvent names must have an earlier corresponding InitEvent`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s
            |> Seq.choose (function InitEvent s -> Some s | _ -> None)
            |> Seq.sort
            |> Seq.toList
        let refEventStrings =
            s
            |> Seq.choose (function RefEvent s -> Some s | _ -> None)
            |> Seq.sort
            |> Seq.toList

        initEventStrings =! refEventStrings

这三个属性都在我的机器上传递。

宽松要求

根据本答案评论中列出的更宽松的要求,这里是一个更新的生成器,它从InitEvents字符串中提取值:

open FsCheck

let streamGen = gen {
    let! uniqueStrings = Arb.Default.NonEmptySet<string>().Generator
    let initEvents = uniqueStrings.Get |> Seq.map InitEvent

    let! refEvents =
        uniqueStrings.Get |> Seq.map RefEvent |> Gen.elements |> Gen.listOf

    return Seq.append initEvents refEvents }

这一次,uniqueStrings是一组非空字符串。

您可以使用Seq.map RefEvent根据RefEvent生成所有有效uniqueStrings值的序列,然后使用Gen.elements定义有效RefEvent的生成器从该有效值序列中提取的值。最后,Gen.listOf创建由该生成器生成的值列表。

测试

这些测试表明streamGen根据规则生成值:

open FsCheck.Xunit
open Swensen.Unquote

let isInitEvent = function InitEvent _ -> true | _ -> false
let isRefEvent =  function RefEvent  _ -> true | _ -> false

[<Property(MaxTest = 100000)>]
let ``All InitEvents must come before all RefEvents`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        test <@ s |> Seq.skipWhile isInitEvent |> Seq.forall isRefEvent @>

[<Property(MaxTest = 100000)>]
let ``All InitEvents strings must be unique`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s |> Seq.choose (function InitEvent s -> Some s | _ -> None)
        let distinctStrings = initEventStrings |> Seq.distinct

        distinctStrings |> Seq.length =! (initEventStrings |> Seq.length)

[<Property(MaxTest = 100000)>]
let ``All RefEvent names must have an earlier corresponding InitEvent`` () =
    Prop.forAll (streamGen |> Arb.fromGen) <| fun s ->
        let initEventStrings =
            s
            |> Seq.choose (function InitEvent s -> Some s | _ -> None)
            |> Seq.sort
            |> Set.ofSeq

        test <@ s
                |> Seq.choose (function RefEvent s -> Some s | _ -> None)
                |> Seq.forall initEventStrings.Contains @>

这三个属性都在我的机器上传递。