Testing in reactive-banana
Asked Answered
U

1

5

Is there a way to unit test networks created in reactive banana? Say I've built up some network with some input events - is it possible to verify that events have produced some output stream/behaviours have some value after some number of input events. Does it even make sense to do this?

I noticed there are various interpret* functions but couldn't seem to work out how to use them. There's also the Model module which looks ideal for testing but has completely different types to the real implementation.

Ufo answered 30/11, 2014 at 17:52 Comment(0)
A
4

When you say "unit test," I'm imagining something like QuickCheck, where you inject a number of inputs into the network and examine the outputs. To do such a thing, we'd need a function along the lines of:

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

Near the end of this answer I demonstrate a variant on one of the interpret* functions that has a similar type, for a specific type of "network."

Mumbo-jumbo about reactive-banana network types

Such a function is incompatible with the actual type of "entire networks" used in reactive-banana. Contrast the type of the actual function involving networks:

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

So the type of any network is forall t. Frameworks t => Moment t (). There are no type variables; no inputs or outputs. Similarly, the EventNetwork type has no parameters. That tells us that all of the input and output is handled via side-effects in IO. It also means there can't really be a function like

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

because what would a and b be?

This is an important aspect of the design of reactive-banana. It makes writing bindings to an imperative GUI framework easy, for instance. The magic of reactive-banana is to shuffle around all the side-effects into, as the docs call it, "a single, huge callback function."

Furthermore, it's typical for the event network to be intimately associated with the GUI itself. Consider the Arithmetic example, where bInput1 and bInput2 are both built using the actual input widgets, and the output is bound to output, another widget.

It would be possible to build a testing harness using "mocking" techniques as in other languages. You could substitute out the actual GUI bindings with bindings to something like pipes-concurrency. I haven't heard of anybody doing so.

How to unit test: Abstract out the logic

Better, you can and should write as much of your program logic as possible in separate functions. If you have two inputs, of types inA and inB, and one output of type out, perhaps you can write a function like

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

This is almost the right type to use with interpretFrameworks:

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

You can combine the two input Events using split (or rather, split the input into the two Events required by logic). Now you'd have logic' :: Event t (Either inA inB) -> Behavior t out.

You're kind of stymied converting the output Behavior to an Event. In version 0.7, the changes function in Reactive.Banana.Frameworks had type Frameworks t => Behavior t a -> Moment t (Event t a), which you could have used to unwrap the Behavior, although you'd have to do it in the Moment monad. In version 0.8, however, the a is wrapped up as a Future a, where Future is an unexported type. (There's an issue on Github re exporting Future.)

The easiest way to unwrap the Behavior is probably just to reimplement interpretFrameworks with the appropriate type. (Note that it returns a tuple containing the initial value and the list of subsequent values.) Even though Future is not exported, you can use its Functor instance:

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)

This should do the trick.

Comparison with other FRP frameworks

Contrast this with other frameworks like Gabriel Gonzalez's mvc or Ertugrul Söylemez's netwire. mvc requires you to write your program logic as a stateful but otherwise-pure Pipe a b (State s) (), and netwire networks have type Wire s e m a b; in both cases the types a and b expose the input to and output from your network. This gets you easy testing, but precludes the "inline" GUI bindings available with reactive-banana. It's a tradeoff.

Amidships answered 30/11, 2014 at 21:20 Comment(7)
Excellent analysis! Regarding Future, this question has a function changes' that removes the future but slightly changes the semantics. From what I can gather from answer, the same value is produced but at a later time. Perhaps we could use this to our advantage? Maybe the change in timing semantics does not matter as this would only be at the "exit point" of our network?Ufo
I was hoping that Heinrich would eventually notice this question :pUfo
Even though Future is not exported, we can use its Functor instance, so implementing interpretFrameworks' is possible. This shouldn't surprise me, but...Amidships
Well, it seems you've cracked it! This gist shows the modified code in action testing a simple behaviour. The only thing is you can't see the initial value of the behaviour, but this is a big leap forward for me (I rely on TDD quite heavily). Thanks!Ufo
Sounds good to me. Just like Christian, I recommend to the test the program logic, i.e. how input Events are transformed into output Events and Behaviors, similar to how you would QuickCheck a pure function. I don't think it's a good idea to mock up GUI widgets -- you could equally well use the GUI itself as a mockup.Braley
It works - I'm also in the process of making a library with some HUnit assertions/Hspec expectation which could go on Hackage.Ufo
Can we not use reactive-banana in the backend ? I don't have any GUI. RxJava etc. don't depend on GUIs. Any examples ?Bespeak

© 2022 - 2024 — McMap. All rights reserved.