3

我使用 FsCheck 进行基于属性的测试,因此我为自定义类型定义了一组生成器。一些类型由其他类型组成,并且所有类型都有生成器。为字母数字类型定义了一个生成器后,我想为 RelativeUrl 类型定义一个生成器,RelativeUrl 是由斜线符号分隔的 1-9 个字母数字值的列表。这是有效的定义(Alpanumeric 具有将其转换为字符串的“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。下一次尝试:

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,而不是 RelativeUrl 的 Gen。那么在不同级别组合生成器的正确方法是什么?

4

2 回答 2

3

Gen.map有签名(f: 'a -> 'b) -> Gen<'a> -> Gen<'b>——也就是说,它接受一个函数 from 'ato 'b,然后是 a Gen<'a>,然后返回 a Gen<'b>。人们可能会将其视为将给定函数“应用”到给定生成器“内部”的内容。

map但实际上,您在调用中提供的函数是int -> Gen<Alphanumeric list>- 也就是说,它返回的不是 some 'b,而是更具体地说Gen<'b>,因此整个表达式的结果变为Gen<Gen<Alphanumeric list>>. 这就是为什么Gen<Alphanumeric list>在 next 中显示为输入的原因map。都是设计使然。

您真正想要的操作通常称为bind. 这样的函数会有一个签名(f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>。也就是说,它将采用一个产生另一个Gen,而不是裸值的函数。

不幸的是,由于某种原因,Gen并没有bind像这样公开。它可以作为gen计算表达式构建器的一部分或作为运算符>>=(它是表示的事实上的标准运算符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))

您也可以考虑使用计算表达式来构建您的生成器。不幸的是,没有wheregen表达式生成器定义,所以你仍然必须使用suchThat来过滤。但幸运的是,有一个特殊的函数Gen.choose可以生成给定范围内的值:

static member RelativeUrl_1() =
  gen {
    // let! length = Arb.generate<int> |> Gen.suchThat (fun l -> l > 0 && l <= 10)
    let! length = Gen.choose (1, 10)
    let! list = Gen.listOfLength length <| Generators.Alphanumeric()
    return String.Join ("/", list)
  }
于 2016-03-31T13:57:20.043 回答
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 值。

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

于 2016-04-01T03:06:15.003 回答