A first look at HMock
The other day I found Chris Smith's HMock: First Rate Mocks in Haskell (link to
hackage) and thought it could be nice see if it can clear up some of the tests I
have in a few of the Haskell projects at work. All the projects follow the
pattern of defining custom monads for effects (something like final tagless)
with instances implemented on a stack of monads from MTL. It's a pretty standard
thing in Haskell I'd say, especially since the monad stack very often ends up
being ReaderT MyConfig IO
.
I decided to try it first on a single such custom monad, one for making HTTP requests:
class Monad m => MonadHttpClient m where mHttpGet :: String -> m (Status, ByteString) mHttpPost :: (Typeable a, Postable a) => String -> a -> m (Status, ByteString)
Yes, the underlying implementation uses wreq, but I'm not too bothered by that
shining through. Also, initially I didn't have that Typeable a
constraint on
mHttpPost
, it got added after a short exchange about KnownSymbol
with Chris.
To dip a toe in the water I thought I'd simply write tests for the two effects themselves. First of all there's an impressive list of extensions needed, and then the monad needs to be made mockable:
{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} makeMockable ''MonadHttpClient
After that, writing a test with HMock for mHttpGet
was fairly straight
forward, I could simply follow the examples in the package's documentation. I'm
using tasty for organising the tests though:
httpGetTest :: TestTree httpGetTest = testCase "Get" $ do (s, b) <- runMockT $ do expect $ MHttpGet "url" |-> (status200, "result") mHttpGet "url" status200 @=? s "result" @=? b
The effect for sending a POST
request was slightly trickier, as can be seen in
the issue linked above, but with some help I came up with the following:
httpPostTest :: TestTree httpPostTest = testCase "Post" $ do (s, b) <- runMockT $ do expect $ MHttpPost_ (eq "url") (typed @ByteString anything) |-> (status201, "result") mHttpPost "url" ("hello" :: ByteString) status201 @=? s "result" @=? b
Next step
My hope is that using HMock will remove the need for creating a bunch of test implementations for the various custom monads for effects1 in the projects, thereby reducing the amount of test code overall. I also suspect that it will make the tests clearer and easier to read, as the behaviour of the mocks are closer to the tests using the mocks.
Footnotes:
Basically they could be looked at as hand-written mocks.