Как вы запускаете асинхронные тесты в 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 ответ
Поведение 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 сгенерирует 100 значений для 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
Несколько ошибок, о которых нужно знать.
Последовательный асинхронный код, как правило, должен создавать небольшие проблемы, но читайте дальше.
Асинхронный и параллельный код может столкнуться с такими проблемами, как сказал Манн в комментариях, т. Е. Несколько потоков / задач используют одно и то же значение. Также будет затронута воспроизводимость. Вы можете тщательно написать свой код свойства, чтобы не сталкиваться с этим (например, имея в своих свойствах прелюдию, в которой все необходимые значения сначала генерируются последовательно, а затем запускаются асинхронные функции), но это требует некоторой работы и думал.
Если вы переопределите
Arbitrary
случаи использованияArb.register
они будут переопределены локальным способом потока; т.е. они не будут распространяться на последовательность асинхронныхTask
s. Мой совет - просто не делай этого. зарегистрированныйArbitrary
экземпляры по существу являются изменяемыми статическими состояниями, и это, как правило, не так уж и приятно при параллельности.
Взятые вместе, я думаю async
свойства определенно возможны, но это определенно что-то вроде тяжелой битвы в v2. FsCheck 3 (в настоящее время в альфа-режиме) поддерживает асинхронное и многопоточное выполнение напрямую.