Testing distributed-process Apps Using Hspec

2018-12-24

distributed-process is a Haskell library that brings Erlang-style concurrency to Haskell. Whilst developing an application at work that uses it, I found that there wasn’t much material online describing how to test distributed-process applications. I used some techniques from object-oriented programming that allowed me to test the behaviour of my application whilst I was learning how it was supposed to fit together. This post documents some techniques I found useful.

Application

Our example revolves around a fairly simple client-server application. The client process can send data to the server and output responses to the console, whilst the server performs calculations and sends the results back to clients.

The code above uses some helper functions that aren’t present in distributed-process; these are included in the code below to let you follow along at home.

Testing

We’ll start off by writing a custom HSpec hook to make a bridge between our application and our tests. Our hook will spin up an application and thread around a shared MVar and a LocalNode.

The MVar serves two purposes; it will be used to communicate state and act as locking mechanism (takeMVar blocks until it’s full). Whilst the LocalNode will allow us to spin up adhoc processes when we need them.

The functions aroundApp and withApp are the first step in bridging these two worlds.

The second step in bridging these worlds is defining a function that’ll listen for messages that are sent to a process and put them in our MVar.

Using these functions, we’ll write our first test. It’s important that we’re confident our application spins up all of the relevant processes it needs to function correctly. We’ll do this by starting our application, checking whether the process is registered and putting the result in our MVar.

From here we’ll want to test that our processes communicate — i.e. send and receive appropriate messages — with one another as we expect. We do this by starting a client process and registering a server test double that’ll listen for messages sent to it using the writer function.

Finally we’ll want to test our server process calculates results correctly and sends them back to clients. We do this by starting our server process and sending a message to it from a test double client process.

Conclusion

The approach described in this post reflects some of my background in object-oriented programming. After all, spinning up processes and testing messages passed between them feels very similar to instantiating objects and doing the same thing.

There are obviously some shortcomings to the techniques described — the big one being that the type checker doesn’t complain when you send an unknown message to a process. That said, the approach distributed-process makes you take is very consistent and makes it pleasant to write asynchronous applications.

Hopefully what I’ve written here offers some insight into how you might begin testing your distributed-process applications.