从相同类型的自定义生成器调用默认FsCheck生成器

时间:2016-04-01 11:42:03

标签: f# fscheck property-based-testing

我定义了一个带有一些自定义生成器的类型,以使FsCheck生成几种类型的自定义实例。但对于其中一种复杂类型,我想首先使用默认的FsCheck生成,然后调整结果。这是一个(简化的)代码:

type CustomGenerators =
    static member FirstCustomType() = /* function that returns FirstCustomType */
    static member SecondCustomType() =
        Arb.generate<SecondCustomType>
        |> Gen.map (fun x -> /* adjust some data in the generated instance */)
        |> Arb.fromGen

问题是,当SecondCustomType()静态方法调用Arb.generate时,它会立即调用SecondCustomType(),从而导致无限递归。我知道Arb.generate必须尊重自定义生成器,所以这就是它调用静态SecondCustomType()的原因,但我需要调用SecondCustomType的默认(非自定义)Arb.generate实现。我不能从不同类型调用实现,因为我的自定义生成器使用自定义生成器用于FirstCustomType,因此默认的SecondCustomType实现必须知道CustomGenerators类型中定义的所有自定义生成器。这是一个糟糕的循环,我还没有找到一个干净的解决方案(只有解决方法)。

1 个答案:

答案 0 :(得分:4)

所有“默认”(即开箱即用)生成器都在FsCheck.Arb.Default类上。根据{{​​1}}实际上的内容,您可以使用该类中的某些方法,例如BoolString

如果您的类型是正确的代数F#类型(即联合,记录或元组),您可以利用自动派生的生成器来处理由Default.Derive表示的类型。

SecondCustomType

话虽如此,我同意Mark上面的评论:使用这些静态方法 - shim-for-type-class生成器总是有点尴尬。就像Mark一样,我更喜欢让FsCheck提供开箱即用的功能,然后使用常规功能组合所需的输入。我给你举个例子。

考虑这种类型,大概不能由FsCheck开箱即用:

type CustomGenerators =
    static member SecondCustomType() =
        Arb.Default.Derive<SecondCustomType>()
        |> Arb.toGen
        |> Gen.map (fun x -> (* adjust some data in the generated instance *) )
        |> Arb.fromGen

这是使用static-shim-for-type-class生成器的尴尬方式:

type SomeAwkwardType( name: string, id: int, flag: bool ) =
   member this.Name = name
   member this.Id = id
   member this.Flag = flag

这是生成输入的更简单(在我看来)的方式:

type AwkwardTypeGenerator() =
   static member Gen() =
      gen {
         let! name = Arb.generate<string>
         let! id = Arb.generate<int>
         let! flag = Arb.generate<bool>
         return SomeAwkwardType( name, id, flag )
      }

module Tests =
   let [Property] ``Test using the awkward generator`` (input: SomeAwkwardType) = 
      someFn input = 42

这不仅更短更清洁,而且还具有一年后不用拔头发的优势,不得不查看实现生成器的静态类的代码库。