FsCheck с установкой и демонтажем

Резюме

Существуют ли какие-либо события, которые можно запускать перед каждым случаем свойства, чтобы я мог запускать установку и демонтаж для каждого запуска свойства?

Полная версия

Я хочу иметь возможность тестировать парное поведение, например, я всегда могу получать письменные записи или выходные данные readAllLines равны входным данным для writeAllLines со свойствами. Я также хочу, чтобы свойство не заботилось о том, как реализованы наборы операций (т. е. нужно ли очищать какие-либо ресурсы).

Каждый запуск свойства должен

  • быть независимым от других запусков
  • поддерживать состояние между вызовами операций в рамках одного запуска
  • не знаю о том, как операции поддерживают состояние
  • не быть утечкой ресурсов

Я использую FsCheck и Expecto. Примеры будут в Expecto, но проблема не во фреймворке.

Очень легко написать такую ​​​​настройку и разборку с помощью тестов, основанных на примерах. Они принимают предсказуемый набор аргументов, поэтому я могу запускать их в оболочке, которая добавляет события до и после.

let testWithEnv setup cleanup name test = 
    let testWrap () = 
        let (api, env) = setup ()
        test api
        cleanup env 
    testCase name testWrap

То же самое нельзя сделать с тестами свойств. У них есть неизвестное количество аргументов, которые в значительной степени будут заполнены случайными данными.

Я могу довольно легко применить набор парных поведений, но любые созданные ресурсы, такие как потоки, остаются неиспользованными.

let testPropertyWithEnv setup cleanup name test = 
    let testWrap () = 
        let (api, env) = setup () // this is actually run once, but immutable so the individual runs don't leak state
        test api // have to return this to pass along unapplied parameters
    testProperty name testWrap

я изучил

Runner events

Глядя на как запускать тесты FsCheck, ближайшие хуки кажутся

  • OnStartFixture который запускается только один раз для каждого тестового класса
  • OnArguments запускается после каждого прохода и потенциально может работать для запуска очистки
Model-based features

Существуют также экспериментальные функции тестирования на основе моделей, которые могут работать. Однако это кажется очень тяжелым, учитывая, что меня интересует только внешняя согласованность операций. Мне не нужен доступ к резервному состоянию.

Giving up and inlining

я всегда могу написать

testProperty "name" (fun arg1 arg2 ->
    let (api,env) = setup ()
    //test code here
    cleanup env
)

но я хотел бы избежать шаблонов в каждом свойстве и разоблачения резервного состояния.

IDisposables

Одноразовые объекты также не решают проблему отсутствия установочного хука.

More hands-on run loop

Я искал способы запуска тестов свойств в своей оболочке, но самый маленький бегун Check.one предназначен для одного свойства, без перехватов между запусками свойства.

Lazy wrapper

Сделать обёртку ленивой тоже не получится testProperty name lazy(testWithSetup)


person farlee2121    schedule 22.12.2020    source источник


Ответы (1)


На самом деле в FsCheck нет ничего, что могло бы вам помочь, и даже если бы оно было, я не думаю, что оно было бы открыто раскрыто в Expecto. Я также не думаю, что это так просто добавить на стороне FsCheck — он сказал, что рад обсудить это дальше, если вы откроете проблему в репозитории FsCheck.

В любом случае, с некоторым умным использованием частичного применения и за счет небольшого шаблонного кода, на самом деле можно обернуть функции с переменным числом аргументов, что, я думаю, по сути, то, что вы просите.

Вот, код:


// these types are here to make the signatures look nicer
type Api = Api
type Env = Env

let testProperty k = 
    // call the property with "random" arguments
    for i in 0..2 do
        k i (char i) (string i)

let setup() = 
    printfn "setup ran"
    (Api, Env)

let teardown Env = 
    printfn "teardown ran"

let test0 Api arg1 = 
    printfn "test0 %A" arg1

let test1 Api (arg1:int) (arg2:char) = 
    printfn "test1 %A %A" arg1 arg2

let test2 Api arg1 arg2 arg3 =
    printfn "testFun %A %A %A" arg1 arg2 arg3

let testWithEnv (setup:unit -> Api*Env) (teardown: Env -> unit) (test: Api -> 'a) (k: 'a -> unit) :unit =
    let (api, env) = setup()
    k (test api)
    teardown env

let (<*>) (f,k) arg  =
    f, (fun c -> k c arg)

let (<!>) f arg =
    f, (fun k -> k arg)

let run (f, k) = f k

testProperty (fun arg1 arg2 arg3 ->
    testWithEnv setup teardown test2 <!> arg1 <*> arg2 <*> arg3 |> run
)

Идея здесь заключается в том, что вы используете операторы <!> и <*> для объединения любого количества аргументов любого типа, передаете это в функцию testWithEnv, а затем вызываете run для результата. Операторы и run в основном необходимы для создания, а затем применения вариативного списка аргументов.

Все это безопасно для типов, т.е. если вы забудете передать аргумент или он имеет неправильный тип, вы получите ошибку типа, хотя, по общему признанию, это может быть не так ясно, как обычное приложение функции.

Я рекомендую вставить это в IDE и проверить типы, которые очень помогут понять, что происходит.

Есть несколько других стилей, в которых вы можете написать это. с немного другим определением и функцией the вы можете написать что-то вроде

let (<+>) k arg  =
    fun c -> k c arg

let the arg =
    fun k -> k arg

testWithEnv setup teardown test2 (the arg1 <+> arg2 <+> arg3)

Это заменяет функцию run на the и требует только одного оператора <+>. Вероятно, есть и другие способы сократить это, выберите свой яд.

person Kurt Schelfthout    schedule 24.12.2020
comment
Это то, что мне нравится в функциональном сообществе. Всегда видя лежащие в основе общие проблемы. Посмотрим, правильно ли я понимаю. Он допускает отложенное выполнение с произвольным числом аргументов, требуя от вызывающего объекта обернуть аргументы в цепочку кортежей функций, которые применяют аргументы по одному. Run затем разматывает цепочку. Это потребовало бы, чтобы каждый из моих тестов свойств вкладывал вызов testWithEnv внутри каждого определения теста свойства. Это также означает, что у меня есть небольшая церемония, обертывающая аргументы, чтобы перейти к внутреннему тесту, который также должен определять аргументы. - person farlee2121; 25.12.2020
comment
Я думаю, что в моем случае я мог бы немного сократить это, используя вложенные области. Я мог бы определить withEnv (f: unit -> bool) и полагаться на внутреннюю функцию для получения всех ее параметров из родительского контекста. Ни один из них не является подпрограммой прямого метода со свойствами, не относящимися к среде, но внутренняя функция довольно инертна для обоих (при условии, что я понимаю вариативную оболочку) - person farlee2121; 25.12.2020
comment
Да, я думаю, вы правильно понимаете. И да, такая функция, как withEnv, тоже будет работать, и, возможно, ее будет легче понять будущим читателям. Вам, вероятно, придется передать что-то внутренней функции? т.е. withEnv (f: Api -> bool) например. - person Kurt Schelfthout; 25.12.2020