如何递归使用FsCheck生成器?

时间:2016-03-31 13:13:15

标签: f# fscheck

我使用FsCheck进行基于属性的测试,因此我为自定义类型定义了一个生成器集。有些类型由其他类型组成,并且所有类型都有生成器。定义了字母数字类型的生成器后,我想为RelativeUrl类型定义生成器,而RelativeUrl是由斜杠符号分隔的1-9个字母数字值的列表。这是有效的定义(Alpanumeric具有将其转换为String的“Value”属性):

static member RelativeUrl() =
    Gen.listOfLength (System.Random().Next(1, 10)) <| Generators.Alphanumeric()
    |> Gen.map (fun list -> String.Join("/", list |> List.map (fun x -> x.Value)) |> RelativeUrl)

即使它非常简单,我也不喜欢使用Random.Next方法而不是使用FsCheck随机生成器。所以我试着像这样重新定义它:

static member RelativeUrl_1() =
    Arb.generate<byte> 
    |> Gen.map int 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    |> Gen.map (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> String.Join("/", list))

编译器接受它,但事实上它是错误的:最后一个语句中的“列表”不是字母数字值的列表,而是Gen.Next尝试:

static member RelativeUrl() =
    Arb.generate<byte> 
    |> Gen.map int 
    |> Gen.suchThat (fun x -> x > 0 && x <= 10)
    |> Gen.map (fun length -> Gen.listOfLength length <| Generators.Alphanumeric())
    |> Gen.map (fun list -> list |> Gen.map (fun elem -> String.Join("/", elem |> List.map (fun x -> x.Value))  |> RelativeUrl))

但这也不起作用:我要回到RelativeUrl Gen的Gen,而不是RelativeUrl的Gen。那么在不同层面组合发电机的正确方法是什么呢?

2 个答案:

答案 0 :(得分:3)

Gen.map具有签名(f: 'a -> 'b) -> Gen<'a> -> Gen<'b> - 也就是说,它需要从'a'b,然后是Gen<'a>的函数,并返回{{} 1}}。人们可能会认为它是&#34;应用&#34;给定的功能是什么&#34;内部&#34;给定的发电机。

但是,您在Gen<'b>来电中提供的功能实际上是map - 也就是说,它不返回某些int -> Gen<Alphanumeric list>,而是更具体地{{1}因此整个表达式的结果变为'b。这就是Gen<'b>在下一个Gen<Gen<Alphanumeric list>>中显示为输入的原因。全部按设计。

您真正想要的操作通常称为Gen<Alphanumeric list>。这样的函数将具有签名map。也就是说,它需要一个产生另一个bind的函数,而不是一个裸值。

不幸的是,由于某些原因,(f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>并未公开Gen。它作为gen computation expression builderoperator >>=的一部分提供(它是表示Gen的事实上的标准运算符)。

鉴于上述解释,您可以像下面这样重新定义您的定义:

bind

您也可以考虑使用计算表达式来构建生成器。不幸的是,bind表达式构建器没有定义static member RelativeUrl_1() = Arb.generate<int> |> Gen.suchThat (fun x -> x > 0 && x <= 10) >>= (fun length -> Gen.listOfLength length <| Generators.Alphanumeric()) |> Gen.map (fun list -> String.Join("/", list)) ,因此您仍然必须使用where进行过滤。但幸运的是,有一个特殊函数gen用于生成给定范围内的值:

suchThat

答案 1 :(得分:2)

Fyodor Soikin的评论表明Gen.choose并不有用,所以也许我错过了一些东西,但这是我的尝试:

open System
open FsCheck

let alphanumericChar = ['a'..'z'] @ ['A'..'Z'] @ ['0'..'9'] |> Gen.elements
let alphanumericString =
    alphanumericChar |> Gen.listOf |> Gen.map (List.toArray >> String)

let relativeUrl = gen {
    let! size = Gen.choose (1, 10)
    let! segments = Gen.listOfLength size alphanumericString
    return String.concat "/" segments }

这似乎有效:

> Gen.sample 10 10 relativeUrl;;
val it : string list =
  ["IC/5p///G/H/ur/vs//"; "l/mGe8spXh//au2WgdL/XvPJhey60X";
   "dxr/0y/1//P93/Ca/D/"; "R/SMJ3BvsM/Fzw4oifN71z"; "52A/63nVPM/TQoICz";
   "Co/1zTNKiCwt1/y6fwDc7U1m/CSN74CwQNl/olneBaJEB/RFqKiCa41l//ADo2MIUPFM/vG";
   "Zm"; "AxRpJ/fP/IOvpX/3yo"; "0/6QuDwiEgC/IpXRO8GA/E7UB8"; "jK/C/X/E4/AL3"]

请注意,我对alphanumericString的定义可能会生成空字符串,因此有时,从上面的FSI示例输出中可以看出,它会生成带有空段的相对URL值。

我将它作为练习留给读者来定义非空的字母数字字符串。如果您需要帮助,请提出另一个问题并打电话给我;)