Mockazo 👃
Mock your records of functions with ease
One approach to structure a Haskell is using records of functions, sometimes called
handles, services, or as we like to call them, components.
Mockazo provides a way of mocking components with ease and to verify that they executed
the proper operations with the proper results.
Adding it to your project
Add the mockazo
dependency to your package.yaml
or your cabal
file.
If you use Stack, you also need to add multistate-0.8.0.2
to your extra-deps
section in the stack.yaml
file.
In your tests, import Data.Component.Mock
, and you are ready to roll!
Some restrictions
For Mockazo to work properly, we need that you do a little tweaking on your component
definitions:
Parametrize the return context
It is common practice to make component methods return values in the IO
context.
This might looks straightforward, but when mocking comes into place, it is much easier
to work in other contexts.
Imagine that we have a simple logging component:
data Component = Component
{ logInfo :: Text -> IO ()
, logWarn :: Text -> IO ()
, logError :: Text -> IO ()
}
For Mockazo to work properly, we parametrize the context of execution:
data Component context = Component
{ logInfo :: Text -> context ()
, logWarn :: Text -> context ()
, logError :: Text -> context ()
}
This not only makes testing easier, but also makes your code much more robust,
because when defining a function that uses this component, we are unable to execute
any other kind of code that runs in another execution contexts (like a colleague
calling launchMissiles :: IO ()
).
All methods must return something in a context
Generally, we use Components to model pieces of our application that perform side effects,
so adding a field that contains some static piece of data doesn't make much sense.
If you really need to do this, we recommend you that you create a companion Configuration
type, with all of these values, and leave the component for the side effect operations only.
If you really need the value inside of the component, wrap it in context
.
Mockazo, expects all the methods to be effectful. So it will choke on a return value that
is not wrapped in the context.
So, instead of doing:
data Component context = Component
{ foo :: Text
}
Do this:
data Component context = Component
{ foo :: context Text
}
Creating your first mock
Let's suppose that we want to mock the logging component from the first example:
data Component context = Component
{ logInfo :: Text -> context ()
, logWarn :: Text -> context ()
, logError :: Text -> context ()
}
Create a separate module for the mock (we recommend you to do it in test/Mock
, and the
name of the module should match the name of the component module).
After that, we create the mock (following the advice, we make it in test/Mock/Logging.hs
):
module Mock.Logging where
import Logging -- We import the component *UNQUALIFIED*
makeMock ''Component
That's it! (Yes, really)
If for some reason, you want to add the export list to the mock module (your compiler is
complaining), you can fix it like this:
module Mock.Logging (Action(..), Component(..), mock) where
import Logging
makeMock ''Component
Testing a function that calls our component
Suppose that somewhere we have a function importantOperation
that looks like this:
importantOperation :: Monad context => Logging.Component context -> context ()
importantOperation Logging.Component{..} = do
logInfo "info"
logWarn "warn"
logError "error"
We want to assure that these operations are run in order and with the
appropriate arguments. We can write a test for it by using Mockazo's little DSL.
In our tests file:
import qualified Mock.Logging as Logging
let loggingMock = Logging.mock
-- ... somewhere in our test framework
runMock
$ withActions
[ Logging.LogInfo "info" :-> ()
, Logging.LogWarn "warn" :-> ()
, Logging.LogError "error" :-> ()
]
$ importantOperation loggingMock
We tell the test to run a function with a mock using runMock
.
After that, we specify the actions that we expect to be run, and what they return,
using the :->
operator, inside of a withActions
block.
Finally, we run the function that we want to test, by passing the mocked component to it.
Functions that depend on multiple components
Suppose that importantOperation
depended on two, three, or whatever more components.
The great stuff about Mockazo, is that you can chain as many withActions
blocks as
you want, passing the expected operations for each one of the mocks.
Suppose that apart from the Logging
component, we had another called UserFetch
.
We could also mock it in the same way we did with Logging
, and add the expected
operations too:
import qualified Mock.Logging as Logging
import qualified Mock.UserFetch as UserFetch
let loggingMock = Logging.mock
let userFetchMock = UserFetch.mock
-- ... somewhere our test framework
runMock
$ withActions
[ Logging.LogInfo "info" :-> ()
, Logging.LogWarn "warn" :-> ()
, Logging.LogError "error" :-> ()
]
$ withActions
[ UserFetch.Connect "localhost" :-> ()
, UserFetch.Fetch :-> User { name = "mike", password = "absolutely-encrypted" }
]
$ importantOperation loggingMock userFetchMock
Acknowledgements
Mockazo is heavily inspired by monad-mock
. It wouldn't have been possible to create this package without it's existence.
To all of the authors and contributors of monad-mock
:
Thank you!