如何在FsCheck中运行异步测试?

时间:2019-01-19 18:44:34

标签: asynchronous f# 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()的更好方法吗?

1 个答案:

答案 0 :(得分:2)

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将其解决为正常值。作为您示例的一种变体:

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类型生成100个值(这些值当然都是(),实际上是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之类的问题,即使用相同值的多个线程/任务。重现性也会受到影响。您也许可以仔细地编写属性代码,这样就不会遇到这种情况(例如,通过在属性中创建序言,先以顺序方式生成所有必需的值,然后启动异步函数),但这需要一些工作和思想。

  • 如果使用Arbitrary覆盖Arb.register个实例,则它们将以 thread 本地方式被覆盖;也就是说,它们不会传播到一系列异步Task上。我的建议是不要这样做。已注册的Arbitrary实例本质上是可变的静态状态,并且通常并不能很好地发挥并发性。

总的来说,我认为async属性是可能的,但这绝对是v2中艰苦的战斗。 FsCheck 3(当前为Alpha版)直接支持异步和多线程执行。