Тестирование в реактивном банане

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

Я заметил, что существуют различные interpret* функции, но не мог понять, как их использовать. Также есть модуль Model, который идеально подходит для тестирования, но имеет совершенно другие типы, чем реальная реализация.


person Callum Rogers    schedule 30.11.2014    source источник


Ответы (1)


Когда вы говорите «модульный тест», я представляю что-то вроде QuickCheck, когда вы вводите в сеть ряд входных данных и проверяете выходные данные. Чтобы сделать это, нам понадобится функция вроде:

evalNetwork :: Network a b -> [a] -> IO [b]

Ближе к концу этого ответа я демонстрирую вариант одной из функций interpret* аналогичного типа для определенного типа «сети».

Мумбо-джамбо о reactive-banana типах сетей

Такая функция несовместима с фактическим типом «целых сетей», используемым в reactive-banana. Сравните тип функции actual, связанной с сетями:

compile :: (forall t. Frameworks t => Moment t ()) -> IO EventNetwork

Итак, тип любой сети forall t. Frameworks t => Moment t (). Переменных типа нет; никаких входов и выходов. Точно так же тип EventNetwork не имеет параметров. Это говорит нам о том, что все входные и выходные данные обрабатываются с помощью побочных эффектов в IO. Это также означает, что на самом деле не может быть такой функции, как

interpret? :: EventNetwork -> [a] -> IO [b]

потому что что будет a и b?

Это важный аспект дизайна reactive-banana. Например, это упрощает написание привязок к императивной структуре графического интерфейса. Магия reactive-banana заключается в том, чтобы смешать все побочные эффекты в, как это называется в документации, «одну огромную функцию обратного вызова».

Кроме того, обычно сеть событий тесно связана с самим графическим интерфейсом. Рассмотрим Arithmetic пример, где bInput1 и bInput2 создаются с использованием фактических виджетов ввода, а вывод привязан к output, другому виджету.

Можно было бы создать систему тестирования, используя методы «насмешки», как и в других языках. Вы можете заменить фактические привязки GUI привязками к чему-то вроде pipes-concurrency. Я не слышал, чтобы кто-то так делал.

Как проводить модульное тестирование: абстрагироваться от логики

Более того, вы можете и должны писать как можно больше логики вашей программы в отдельных функциях. Если у вас есть два входа типов inA и inB и один выход типа out, возможно, вы можете написать такую ​​функцию, как

logic :: Event t inA -> Event t inB -> Behavior t out

Это почти правильный тип для использования с interpretFrameworks:

interpretFrameworks :: (forall t. Event t a -> Event t b) ->
                       [a] -> IO [[b]]

Вы можете объединить два входных Event с помощью split (точнее, разделить вход на два Event, необходимых для logic). Теперь у вас будет logic' :: Event t (Either inA inB) -> Behavior t out.

Вы как бы зашли в тупик, преобразовывая вывод Behavior в Event. В версии 0.7 функция changes в Reactive.Banana.Frameworks имела тип Frameworks t => Behavior t a -> Moment t (Event t a), который вы могли использовать для развертывания Behavior, хотя вам пришлось бы делать это в монаде Moment. Однако в версии 0.8 a обернут как Future a, где Future — неэкспортируемый тип. (На Github есть проблема с повторным экспортом Future.)

Вероятно, самый простой способ развернуть Behavior — просто переопределить interpretFrameworks с соответствующим типом. (Обратите внимание, что он возвращает кортеж, содержащий начальное значение и список последующих значений.) Хотя Future не экспортируется, вы можете использовать его экземпляр Functor:

interpretFrameworks' :: (forall t. Event t a -> Behavior t b) 
                        -> [a] -> IO (b, [[b]])
interpretFrameworks' f xs = do
    output                    <- newIORef []
    init                      <- newIORef undefined
    (addHandler, runHandlers) <- newAddHandler
    network                   <- compile $ do
        e <- fromAddHandler addHandler
        o <- changes $ f e
        i <- initial $ f e
        liftIO $ writeIORef init i
        reactimate' $ (fmap . fmap) (\b -> modifyIORef output (++[b])) o

    actuate network
    bs <- forM xs $ \x -> do
        runHandlers x
        bs <- readIORef output
        writeIORef output []
        return bs
    i <- readIORef init
    return (i, bs)

Это должно сработать.

Сравнение с другими фреймворками FRP

Сравните это с другими фреймворками, такими как mvc Габриэля Гонсалеса или netwire. mvc требует, чтобы вы написали логику вашей программы как состояние, но в остальном чистое Pipe a b (State s) (), а netwire сети имеют тип Wire s e m a b; в обоих случаях типы a и b предоставляют ввод и вывод из вашей сети. Это упрощает тестирование, но исключает встроенные привязки графического интерфейса, доступные с reactive-banana. Это компромисс.

person Christian Conkle    schedule 30.11.2014
comment
Отличный анализ! Что касается Future, этот вопрос имеет функцию changes', которая удаляет будущее, но немного меняет семантику. Из того, что я могу понять из ответа, создается то же значение, но в более позднее время. Возможно, мы могли бы использовать это в наших интересах? Может быть, изменение временной семантики не имеет значения, так как это будет только в точке выхода из нашей сети? - person Callum Rogers; 01.12.2014
comment
Я надеялся, что Генрих в конце концов заметит этот вопрос :p - person Callum Rogers; 01.12.2014
comment
Несмотря на то, что Future не экспортируется, мы можем использовать его экземпляр Functor, поэтому возможна реализация interpretFrameworks'. Это не должно меня удивлять, но... - person Christian Conkle; 01.12.2014
comment
Ну, кажется, вы его взломали! Это краткое описание показывает модифицированный код в действии, проверяющий простое поведение. Единственное, вы не можете видеть начальное значение поведения, но для меня это большой шаг вперед (я довольно сильно полагаюсь на TDD). Спасибо! - person Callum Rogers; 01.12.2014
comment
Звучит неплохо. Как и Кристиан, я рекомендую тестировать логику программы, т. е. то, как входные события преобразуются в выходные события и поведения, подобно тому, как вы бы быстро проверили чистую функцию. Я не думаю, что это хорошая идея — макетировать виджеты графического интерфейса — вы с таким же успехом можете использовать сам графический интерфейс в качестве макета. - person Heinrich Apfelmus; 02.12.2014
comment
Это работает - я также нахожусь в процессе создания библиотеки с некоторыми утверждениями HUnit / ожиданиями Hspec, которые могут быть реализованы в Hackage. - person Callum Rogers; 03.12.2014