如何递归使用 FsCheck 生成器?
How to recursively use FsCheck generators?
我使用 FsCheck 进行基于 属性 的测试,因此我为自定义类型定义了一组生成器。有些类型由其他类型组成,并且所有类型都有生成器。为字母数字类型定义了一个生成器后,我想为 RelativeUrl 类型定义一个生成器,而 RelativeUrl 是由斜杠符号分隔的 1-9 个字母数字值的列表。这是有效的定义(字母数字有 "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))
编译器接受它,但实际上它是错误的:最后一个语句中的 "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 of Gen,而不是 RelativeUrl 的 Gen。那么在不同级别组合生成器的正确方法是什么?
Gen.map
具有签名 (f: 'a -> 'b) -> Gen<'a> -> Gen<'b>
- 也就是说,它采用从 'a
到 'b
的函数,然后是 Gen<'a>
,以及 returns一个Gen<'b>
。人们可能会认为它是 "applying" 给定生成器的 "inside" 的给定函数。
但是您在 map
调用中提供的功能实际上是 int -> Gen<Alphanumeric list>
- 也就是说,它 returns 不是某些 'b
,而是更多特别是 Gen<'b>
,所以整个表达式的结果变成 Gen<Gen<Alphanumeric list>>
。这就是为什么 Gen<Alphanumeric list>
在下一个 map
中显示为输入的原因。完全由设计决定。
您真正想要的操作通常称为bind
。这样的函数会有一个签名(f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>
。也就是说,它需要一个函数来生成另一个 Gen
,而不是一个裸值。
不幸的是,出于某种原因,Gen
没有公开 bind
。它可作为 gen
computation expression builder or as operator >>=
的一部分使用(它实际上是表示 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))
您也可以考虑使用计算表达式来构建生成器。不幸的是,没有为 gen
表达式生成器定义 where
,因此您仍然必须使用 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)
}
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 值。
我将把它作为练习留给 reader 来定义非空字母数字字符串。如果您需要这方面的帮助,请提出另一个问题并联系我 ;)
我使用 FsCheck 进行基于 属性 的测试,因此我为自定义类型定义了一组生成器。有些类型由其他类型组成,并且所有类型都有生成器。为字母数字类型定义了一个生成器后,我想为 RelativeUrl 类型定义一个生成器,而 RelativeUrl 是由斜杠符号分隔的 1-9 个字母数字值的列表。这是有效的定义(字母数字有 "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))
编译器接受它,但实际上它是错误的:最后一个语句中的 "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 of Gen,而不是 RelativeUrl 的 Gen。那么在不同级别组合生成器的正确方法是什么?
Gen.map
具有签名 (f: 'a -> 'b) -> Gen<'a> -> Gen<'b>
- 也就是说,它采用从 'a
到 'b
的函数,然后是 Gen<'a>
,以及 returns一个Gen<'b>
。人们可能会认为它是 "applying" 给定生成器的 "inside" 的给定函数。
但是您在 map
调用中提供的功能实际上是 int -> Gen<Alphanumeric list>
- 也就是说,它 returns 不是某些 'b
,而是更多特别是 Gen<'b>
,所以整个表达式的结果变成 Gen<Gen<Alphanumeric list>>
。这就是为什么 Gen<Alphanumeric list>
在下一个 map
中显示为输入的原因。完全由设计决定。
您真正想要的操作通常称为bind
。这样的函数会有一个签名(f: 'a -> Gen<'b>) -> Gen<'a> -> Gen<'b>
。也就是说,它需要一个函数来生成另一个 Gen
,而不是一个裸值。
不幸的是,出于某种原因,Gen
没有公开 bind
。它可作为 gen
computation expression builder or as operator >>=
的一部分使用(它实际上是表示 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))
您也可以考虑使用计算表达式来构建生成器。不幸的是,没有为 gen
表达式生成器定义 where
,因此您仍然必须使用 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)
}
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 值。
我将把它作为练习留给 reader 来定义非空字母数字字符串。如果您需要这方面的帮助,请提出另一个问题并联系我 ;)