你如何 运行 在 FsCheck 中进行异步测试?
How do you run async tests in FsCheck?
如何使用 FsCheck 获得可重复的异步测试?这是我在 FSI 中 运行 的示例代码:
let prop_simple() = gen {
let! s = Arb.generate<string>
printfn "simple: s = %A" s
return 0 < 1
}
let prop_async() =
async {
let s = Arb.generate<string> |> Gen.sample 10 1 |> List.head
// let! x = save_to_db s // for example
printfn "async: s = %A" s
return 0 < 1
}
|> Async.RunSynchronously
let check_props() =
//FC2.FsCheckModifiers.Register()
let config =
{ FsCheck.Config.Default with
MaxTest = 5
Replay = Random.StdGen(952012316,296546221) |> Some
}
Check.One(config, prop_simple)
Check.One(config, prop_async)
输出看起来像这样:
simple: s = "VDm2JQs5z"
simple: s = "NVgDf2mQs8zaWELndK"
simple: s = "TWz3Yjl2tHFERyrMTvl0HOqgx"
simple: s = "KRWC92vBdZAHj6qcf"
simple: s = "CTJbQGXzpLBNn0RY6MCvlfUtbQhCUKm9tbXFhLSu0RcYmi"
Ok, passed 5 tests.
async: s = "aOE"
async: s = "y8"
async: s = "y8"
async: s = "q"
async: s = "q"
Ok, passed 5 tests.
另一个 运行 可以是这样的:
simple: s = "VDm2JQs5z"
simple: s = "NVgDf2mQs8zaWELndK"
simple: s = "TWz3Yjl2tHFERyrMTvl0HOqgx"
simple: s = "KRWC92vBdZAHj6qcf"
simple: s = "CTJbQGXzpLBNn0RY6MCvlfUtbQhCUKm9tbXFhLSu0RcYmi"
Ok, passed 5 tests.
async: s = "g"
async: s = "g"
async: s = "g"
async: s = ""
async: s = ""
Ok, passed 5 tests.
所以 prop_simple()
工作正常并且是可重复的(给定 StdGen(952012316,296546221)
)。
但是 prop_async()
不可重复并且似乎一遍又一遍地生成相同的字符串。
另外,prop_async()
有没有更好的写法?
FsCheck 的行为实际上与此处的 async
没有任何关系,而是与 async
中您正在使用 Gen.sample
的事实有关。 Gen.sample
为每次调用选择一个新的基于时间的种子 - 因此它在 FsCheck 属性 中的行为是不可重现的。换句话说,你不应该在 属性 中使用它,它只是在你编写新生成器时用于探索目的。由于种子是基于时间的,并且您的 属性 非常小,因此多次调用将使用相同的种子,因此您会看到相同的值。例如,这里有一个 属性 没有任何具有相同行为的 async
:
let prop_simple2() =
let s = Arb.generate<string> |> Gen.sample 10 1 |> List.head
// let! x = save_to_db s // for example
printfn "simple2: s = %A" s
0 < 1
打印例如
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
Ok, passed 5 tests.
现在至于如何写一个 async
属性,我会在 属性 中保持异步,然后使用 Async.RunSynchronously
将其解析为正常的 value.As 你的例子的一个变体:
let prop_async2 =
gen {
let! s = Arb.generate<string>
// let! x = save_to_db s // for example
let r =
async {
printfn "async2: s = %A" s
}
|> Async.RunSynchronously
return 0 < 1
}
具有确定性输出。 (另请注意,如果您已经创建了一个 Gen<'T>
实例,则不需要将 属性 设为函数。您可以,但这仅意味着 FsCheck 将为 unit
类型(这些值当然都是 ()
,实际上是 null
,所以它不会造成伤害,但会带来很小的性能提升。)
你也可以反过来做:
let prop_async3 =
async {
let r = gen {
let! s = Arb.generate<string>
printfn "async3: s = %A" s
return 0 < 1
}
return r
}
|> Async.RunSynchronously
需要注意的几个陷阱。
顺序异步代码通常不会造成什么问题,但请继续阅读。
异步和 并发 代码可能 运行 出现像 munn 在评论中所说的问题,即多个 threads/tasks 使用相同的值.重现性也会受到影响。您可以仔细编写您的 属性 代码,这样您就不会 运行 进入这个(例如,通过在您的属性中有一个序曲,其中所有必要的值首先以顺序方式生成,然后启动异步功能),但它需要一些工作和思考。
如果您使用 Arb.register
覆盖 Arbitrary
个实例,它们将以 thread 本地方式被覆盖;也就是说,它们不会传播到异步 Task
序列。我的建议是不要那样做。已注册的 Arbitrary
实例本质上是可变的静态状态,通常不会很好地处理并发。
综上所述,我认为 async
属性绝对是可能的,但这在 v2 中绝对是一场艰苦的战斗。 FsCheck 3(目前处于 alpha 阶段)直接支持异步和多线程执行。
如何使用 FsCheck 获得可重复的异步测试?这是我在 FSI 中 运行 的示例代码:
let prop_simple() = gen {
let! s = Arb.generate<string>
printfn "simple: s = %A" s
return 0 < 1
}
let prop_async() =
async {
let s = Arb.generate<string> |> Gen.sample 10 1 |> List.head
// let! x = save_to_db s // for example
printfn "async: s = %A" s
return 0 < 1
}
|> Async.RunSynchronously
let check_props() =
//FC2.FsCheckModifiers.Register()
let config =
{ FsCheck.Config.Default with
MaxTest = 5
Replay = Random.StdGen(952012316,296546221) |> Some
}
Check.One(config, prop_simple)
Check.One(config, prop_async)
输出看起来像这样:
simple: s = "VDm2JQs5z"
simple: s = "NVgDf2mQs8zaWELndK"
simple: s = "TWz3Yjl2tHFERyrMTvl0HOqgx"
simple: s = "KRWC92vBdZAHj6qcf"
simple: s = "CTJbQGXzpLBNn0RY6MCvlfUtbQhCUKm9tbXFhLSu0RcYmi"
Ok, passed 5 tests.
async: s = "aOE"
async: s = "y8"
async: s = "y8"
async: s = "q"
async: s = "q"
Ok, passed 5 tests.
另一个 运行 可以是这样的:
simple: s = "VDm2JQs5z"
simple: s = "NVgDf2mQs8zaWELndK"
simple: s = "TWz3Yjl2tHFERyrMTvl0HOqgx"
simple: s = "KRWC92vBdZAHj6qcf"
simple: s = "CTJbQGXzpLBNn0RY6MCvlfUtbQhCUKm9tbXFhLSu0RcYmi"
Ok, passed 5 tests.
async: s = "g"
async: s = "g"
async: s = "g"
async: s = ""
async: s = ""
Ok, passed 5 tests.
所以 prop_simple()
工作正常并且是可重复的(给定 StdGen(952012316,296546221)
)。
但是 prop_async()
不可重复并且似乎一遍又一遍地生成相同的字符串。
另外,prop_async()
有没有更好的写法?
FsCheck 的行为实际上与此处的 async
没有任何关系,而是与 async
中您正在使用 Gen.sample
的事实有关。 Gen.sample
为每次调用选择一个新的基于时间的种子 - 因此它在 FsCheck 属性 中的行为是不可重现的。换句话说,你不应该在 属性 中使用它,它只是在你编写新生成器时用于探索目的。由于种子是基于时间的,并且您的 属性 非常小,因此多次调用将使用相同的种子,因此您会看到相同的值。例如,这里有一个 属性 没有任何具有相同行为的 async
:
let prop_simple2() =
let s = Arb.generate<string> |> Gen.sample 10 1 |> List.head
// let! x = save_to_db s // for example
printfn "simple2: s = %A" s
0 < 1
打印例如
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
simple2: s = "nrP?.PFh^y"
Ok, passed 5 tests.
现在至于如何写一个 async
属性,我会在 属性 中保持异步,然后使用 Async.RunSynchronously
将其解析为正常的 value.As 你的例子的一个变体:
let prop_async2 =
gen {
let! s = Arb.generate<string>
// let! x = save_to_db s // for example
let r =
async {
printfn "async2: s = %A" s
}
|> Async.RunSynchronously
return 0 < 1
}
具有确定性输出。 (另请注意,如果您已经创建了一个 Gen<'T>
实例,则不需要将 属性 设为函数。您可以,但这仅意味着 FsCheck 将为 unit
类型(这些值当然都是 ()
,实际上是 null
,所以它不会造成伤害,但会带来很小的性能提升。)
你也可以反过来做:
let prop_async3 =
async {
let r = gen {
let! s = Arb.generate<string>
printfn "async3: s = %A" s
return 0 < 1
}
return r
}
|> Async.RunSynchronously
需要注意的几个陷阱。
顺序异步代码通常不会造成什么问题,但请继续阅读。
异步和 并发 代码可能 运行 出现像 munn 在评论中所说的问题,即多个 threads/tasks 使用相同的值.重现性也会受到影响。您可以仔细编写您的 属性 代码,这样您就不会 运行 进入这个(例如,通过在您的属性中有一个序曲,其中所有必要的值首先以顺序方式生成,然后启动异步功能),但它需要一些工作和思考。
如果您使用
Arb.register
覆盖Arbitrary
个实例,它们将以 thread 本地方式被覆盖;也就是说,它们不会传播到异步Task
序列。我的建议是不要那样做。已注册的Arbitrary
实例本质上是可变的静态状态,通常不会很好地处理并发。
综上所述,我认为 async
属性绝对是可能的,但这在 v2 中绝对是一场艰苦的战斗。 FsCheck 3(目前处于 alpha 阶段)直接支持异步和多线程执行。