How to test Async OCaml code
18 Sep 2014In the previous blog post, we used Core and Async to write a tiny library for talking to Memcached using the binary protocol. I wanted to write tests for the library in a readable and succint manner to ensure correctness – the type system cannot ensure binary data is parsed correctly after all. The regular go-to tool for testing OCaml code is OUnit, but this doesn’t work well with Async. As I couldn’t find a suitable library, I decided to write something myself.
The Goal
My goal was to write a tiny testing framework with the following features:
- Easily write tests for code using Async.
- Assertions with informative output and type-aware equality.
- Before test suite setup function, e.g. connect to Memcached.
- Before test setup function, e.g. prepare some data in Memcached.
- After test teardown function, e.g. flush Memcached.
- After test suite teardown function, e.g. close connection to Memcached.
Inspired by Command, I also wanted write the tests in a DSL-like manner. This is an example of the end goal:
Here we’re using Local Opens to avoid prefixing function names with the module name all the time, i.e. Async_suite.(f x y)
is the same as Async_suite.f x y
. We’re also using the pipe function x |> f
, which is the same as f x
.
The Implementation
The type of a test suite is parameterized over the argument for the before_all
and after_all
functions, and the argument for the before
and after
functions:
Note that a test function is expected to return a unit Deferred.t
that resolves when the test is completed and should raise AssertionFailure
if an assertion fails. For brevity I’ll write ('suite_params, 'params) t
as ('a, 'b) t
henceforth.
Creating the test suite and adding tests is straight forward:
Now we’re just missing functionality to run the tests:
In brief, iterate sequentially over each test function while making sure to call the before- and after-handlers at appropriate times. Test functions are expected to raise AssertionFailure
if the test fails, which is then caught and printed – otherwise the test is deemed a pass.
Writing Assertions
Raising AssertionFailure
can be done writing plain OCaml code, but I’ve found it very helpful to define some assertion-functions myself. The following code makes it easy to assert on values of type Deferred.Or_error.t
(doc), which is used in the Memcached-client, and get helpful output on failures:
The signature Assertable
ensures we can compare and print values of some type t
. This signature is coincidentally fulfilled by all the common data types in Core. The functor MakeAsserter
allows us to create asserters for any Assertable
with suitable behavior.
These asserters make it easy to write concise assertions:
Wrap up
In this blog post we wrote a tiny library for testing code that uses Async. The resulting library makes it easy to write concise tests with life-cycle hooks and helpful test output. I wrote this out of a need to test Async code and couldn’t find a suitable test library, but if I missed something, please write a comment and enlighten me.
If you like this post, please vote on Hacker News.