Как вы запускаете асинхронные тесты в FsCheck? - PullRequest
0 голосов
/ 19 января 2019

Как я могу получить повторяющиеся асинхронные тесты с помощью 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 голосов
/ 20 января 2019

Поведение 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, они будут переопределены thread локальным способом; то есть они не будут распространяться на последовательность асинхронных Task с. Мой совет - просто не делай этого. Зарегистрированные Arbitrary экземпляры являются по существу изменяемым статическим состоянием, и это, как правило, не очень хорошо сочетается с одновременностью.

Взятые вместе, я думаю, async свойства определенно возможны, но это определенно что-то вроде тяжелой битвы в v2. FsCheck 3 (в настоящее время в альфа-режиме) напрямую поддерживает асинхронное и многопоточное выполнение.

...