24 Days of Hackage: doctest

Testing and documentation. Two words that will make even the most hardened programmers shudder. Unfortunately, they are ultimately two of the most important aspects of programming - especially if you want your work to succeed in the wild. Even if you practice test-driven development religiously, you still can’t rule out writing documentation. If only there was a method to combine the two…

Simon Hengel’s doctest library is one solution that can ease this pain. Modelled off the doctest library for Python, doctest embeds tests inside the documentation of modules. The idea is: if testing requires code to be tested and an expected result, then we can treat this as an expected interaction at a REPL. So the work required by the programmer is to enter the input and output of a REPL session. doctest then parses the documentation and runs the code, checking that your expectation matches reality. If so, then it naturally follows that your documentation is consistent with what the library does.

doctest for Haskell works using Haddock’s documentation strings. For example, we can easily check that a square function does indeed square its input:

{-| Given an integer, 'square' returns the same number squared:

>>> square 5
25
-}
square :: Int -> Int
square x = x * x

To run this, we have two options. One is to simply use the doctest executable:

> doctest 2013-12-18-square.hs
Examples: 1  Tried: 1  Errors: 0  Failures: 0

However, in bigger projects you’ll want to integrate doctests into your Cabal file. This means that running cabal test will also run doctests, which makes it harder to forget to run them. To do this, we just have to build a little executable that calls the doctest function:

main :: IO ()
main = doctest [ "2013-12-17-square.hs" ]

The output of this is the same, but we can now easily integrate this as a test-suite in a Cabal file.

So far we’ve only see a simple test of a pure function, but doctest can go a lot further. For example, we might have a function that requires a callback. In a GHCI session, we might write this callback in a let binding, and we can do the same in doctest. The function we are testing will work in the IO monad, and the expected behaviour is to print to standard output. This is naturally expressed in doctest - we just write out what we’d expect to see in GHCI:

{-|
>>> :{
      let callback name = do
            putStrLn $ "Hello. Yes, this is " ++ name
>>> :}

>>> printer "Dog" callback
Dog says:
Hello. Yes, this is Dog
-}
printer :: String -> (String -> IO ()) -> IO ()
printer name callBack = do
  putStrLn $ name ++ " says:"

Observant readers will not that this isn’t quite what it claims - and doctest notices that too:

### Failure in 2013-12-18-print.hs:9: expression `printer "Dog" callback'
expected: Dog says:
          Hello. Yes, this is Dog
 but got: Dog says:

Whoops, looks like we forgot to actually call the callback! If we fix that, we get a happy result from doctest once again.

If you’re a library author, I highly recommend you give doctest ago - it’s usage really is a net win. You get more guarantees that your library is doing what is expected of it, and you get even more back if you encourage your users to help write documentation. By writing examples for you, they’ll also be writing test cases - and probably test cases for the things they care about too.

Today’s code can be found on Github.


You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.