This section describes how to customize the output of Hspec by writing a custom formatter. The focus is on how to write and package a formatter so that it can be used by others.
The essence of a formatter in Hspec is a function format :: Format
. This
function takes an Event
and produces some side effect:
type Format = Event -> IO ()
As an example here is a format function that, on each completed spec item,
prints the item path and one of ✔
/‐
/✘
:
format :: Format
format event = case event of
ItemDone path item -> putStrLn (formatItem path item)
_ -> return ()
where
formatItem :: Path -> Item -> String
formatItem path item = joinPath path <> " [" <> formatResult item <> "]"
formatResult :: Item -> String
formatResult item = case itemResult item of
Success {} -> "✔"
Pending {} -> "‐"
Failure {} -> "✘"
joinPath :: Path -> String
joinPath (groups, requirement) = intercalate " ❯ " $ groups ++ [requirement]
When we run a spec with this formatter we anticipate output of the form:
reverse ❯ reverses a list [✔]
reverse ❯ gives the original list, if applied twice [✔]
To use a formatter, we first need to register it; not the format
function
itself, but something that offers more utility:
formatter :: (String, FormatConfig -> IO Format)
formatter = ("my-formatter", \ _config -> return format)
--format=NAME
. Here, we use "my-formatter"
as the name.FormatConfig
to the formatter so
that the format
function can make use of it. Here, we don't
look at the config value at all.format
function itself to be constructed in IO
. Here,
we simply return the format
function without doing any IO
.This formatter
can then be used with hspecWith
:
main :: IO ()
main = hspecWith (useFormatter formatter defaultConfig) spec
Note: A more idiomatic way to use formatters, that plays nice with hspec-discover
,
is discussed below.
runhaskell Spec.hs reverse ❯ reverses a list [✔] reverse ❯ gives the original list, if applied twice [✔]
It is possible to use a custom monad stack for the format
function.
As an example, let's assume we want to enumerate spec items, changing the output from above to:
1. reverse ❯ reverses a list [✔]
2. reverse ❯ gives the original list, if applied twice [✔]
Using StateT Int IO
instead of IO
is one way to achieve this:
format :: Event -> StateT Int IO ()
format event = case event of
ItemDone path item -> do
n <- state (id &&& succ)
liftIO $ putStrLn (show n <> ". " <> formatItem path item)
_ -> return ()
The monadic
function can be used to transform a monadic format
function
into one of type IO Format
. To do this, monadic
requires some function:
run :: MonadIO m => m () -> IO ()
For StateT Int IO
we can use evalStateT
:
formatter :: (String, FormatConfig -> IO Format)
formatter = ("my-formatter", \ _config -> monadic (`evalStateT` 1) format)
runhaskell Spec.hs 1. reverse ❯ reverses a list [✔] 2. reverse ❯ gives the original list, if applied twice [✔]
The previous examples used hspecWith
to register a formatter. This
works, but it requires the user to write a custom main
function. This is
inconvenient when you are using hspec-discover
.
modifyConfig
provides an alternative, it can be used to register a
formatter in SpecM
:
spec :: Spec
spec = do
modifyConfig (useFormatter MyFormatter.formatter)
...
modifyConfig
can be used anywhere within a spec. However, it is idiomatic
to use it from a spec hook:
-- file test/SpecHook.hs
module SpecHook where
import Test.Hspec
import Test.Hspec.Api.Format.V1 (modifyConfig, useFormatter)
import qualified MyFormatter
hook :: Spec -> Spec
hook = (modifyConfig (useFormatter MyFormatter.formatter) >>)
When distributing a formatter you should, by convention, provide at least three primitives:
module MyFormatter (use, register, formatter, module Api) where
import Test.Hspec.Api.Format.V1 as Api
-- | Make `formatter` available for use with @--format@ and use it by default.
use :: SpecWith a -> SpecWith a
use = (modifyConfig (useFormatter formatter) >>)
-- | Make `formatter` available for use with @--format@.
register :: SpecWith a -> SpecWith a
register = (modifyConfig (registerFormatter formatter) >>)
formatter :: (String, FormatConfig -> IO Format)
formatter = ...
use
and register
make it convenient to register the formatter from a
spec hook:
The formatter
should be exported so that it is possible to augment it if
needed. In addition you may choose to export internals that are specific to
your formatter.
You also want to re-export Test.Hspec.Api.Format.V1
for two reasons:
runhaskell -isrc -itest test/Spec.hs Data.List ❯ reverse ❯ reverses a list [✔] Data.List ❯ reverse ❯ gives the original list, if applied twice [✔]