Description

# Introduction and motivation

This package provides a set of helper functions and types that are designed to assist with writing tests for functions that encode side-effects into monads using effect-specific typeclasses. Consider a function that performs some sort of side effect, such as a function that looks up a user from a database:

lookupUser :: UserId -> IO (Maybe User)

Now consider a function that uses the lookupUser function:

lookupUserIsAdmin :: UserId -> IO Bool
maybeUser <- lookupUser userId
return $maybe False isAdmin maybeUser This function works fine, but it’s very difficult to test, even though it is extremely simple. Since lookupUser just runs in IO, it isn’t easy to test lookupUserIsAdmin in isolation. To fix this, it’s possible to create a layer of indirection between lookupUserIsAdmin and lookupUser by making lookupUser a method of a typeclass instead of a free function: class Monad m => LookupUser m where lookupUser :: UserId -> m (Maybe User) Implementing the original, IO-bound version of lookupUser is easy; we just create a LookupUser instance for IO: instance LookupUser IO where lookupUser = lookupUserIO However, we can also create other monads that implement the LookupUser typeclass. For example, we could create a very simple newtype wrapper around Identity with an implementation that always returns a user successfully: newtype SuccessMonad a = SuccessMonad (Identity a) deriving (Functor, Applicative, Monad) runSuccess :: SuccessMonad a -> a runSuccess (SuccessMonad (Identity x)) = x instance LookupUser SuccessMonad where lookupUser _ = return$ Just User { isAdmin = True }

Now we can test lookupUserIsAdmin completely deterministically without ever needing to touch a real database (using hspec syntax as an example):

lookupUserIsAdmin :: LookupUser m => UserId -> m Bool
maybeUser <- lookupUser userId
return $maybe False isAdmin maybeUser spec = describe "lookupUserIsAdmin"$ do
it "returns True when the UserId corresponds to an admin user" $runSuccess (lookupUserIsAdmin (UserId 42)) shouldBe True Similarly, we can also test the failure case by creating a monad that will always return Nothing: newtype FailureMonad a = FailureMonad (Identity a) deriving (Functor, Applicative, Monad) runFailure :: FailureMonad a -> a runFailure (FailureMonad (Identity x)) = x instance LookupUser FailureMonad where lookupUser _ = return Nothing it "returns False when the UserId does not have a corresponding User"$
runFailure (lookupUserIsAdmin (UserId 42)) shouldBe False

This is great, but it comes at a pretty significant cost: lots and lots of boilerplate. It could get even worse when you have a typeclass with many methods, or even multiple typeclasses at a time! Clearly, there needs to be some way to abstract this pattern a little bit to make it easier to use.

To permit creating easily customizable implementations of monadic interfaces, we can reify a typeclass at the value level by creating a record type with a field that corresponds to each method:

data Fixture m = Fixture { _lookupUser :: UserId -> m (Maybe User) }

We have to prefix each method name with an underscore to avoid name clashes, but now we have the ability to create a first-class value that represents a particular implementation of the LookupUser typeclass. The next step is turning one of these values into something that can actually be supplied as a monad implementation. One way to do this is to use a reader monad to thread a particular Fixture value around. We can create a newtype that will do that for us:

newtype FixtureM a = FixtureM (Fixture Identity -> a)

runFixture :: Fixture Identity -> FixtureM a -> a
runFixture fixture (FixtureM func) = func fixture

By making this new FixtureM type an instance of LookupUser, we can use the runFixture function that we defined to run a particular computation with any arbitrary fixture at runtime:

instance LookupUser FixtureM where
lookupUser userId = FixtureM $\fixture -> runIdentity$ _lookupUser fixture userId

Now we can write all our tests using one-off fixture implementations without creating entirely new types:

spec = describe "lookupUserIsAdmin" $do it "returns True when the UserId corresponds to an admin user"$ do
let fixture = Fixture { _lookupUser = return $Just User { isAdmin = True } } runFixture fixture (lookupUserIsAdmin (UserId 42)) shouldBe True it "returns False when the UserId corresponds to a non-admin user"$ do
let fixture = Fixture { _lookupUser = return $Just User { isAdmin = False } } runFixture fixture (lookupUserIsAdmin (UserId 42)) shouldBe False it "returns False when the UserId does not have a corresponding User"$ do
let fixture = Fixture { _lookupUser = return Nothing }
runFixture fixture (lookupUserIsAdmin (UserId 42)) shouldBe False

The above example is relatively contrived, but it may be possible to see how this technique could be applied to a larger set of monadic typeclasses by creating more instances on a fixture with more methods.

However, it is sometimes useful to do even more with a fixture, such as verifying that a given function was called with a particular argument. For example, consider a function with the following signature:

insertUser :: User -> m ()

In this case, testing the result is likely not particulary interesting, but testing that the function itself is called with the right argument might be helpful. Even more subtly, a function might be called multiple times, and it might need to return different values each time! This requires some degree of state tracking that a reader monad simply cannot provide.

To solve this, the provided TestFixture monad is a wrapper aroud the RWS monad, which combines a reader, writer, and state monad into a single system. This allows “logging” results from a fixture by using tell within the fixture definition and logTestFixture, and it also permits having fixture invocations depend on previous uses of the fixture by using get and put from MonadState.

Continuing from the above example but using TestFixture instead, we eschew the simpler FixtureM type and create instances over TestFixture instead:

instance Monoid w => LookupUser (TestFixture Fixture w s) where
lookupUser userId = do
lift $fn userId Now we can write our tests using the unTestFixture function, along with the similar logTestFixture functions and friends: spec = describe "lookupUserIsAdmin"$ do
it "returns True when the UserId corresponds to an admin user" $do let fixture = Fixture { _lookupUser = return$ Just User { isAdmin = True } }
unTestFixture (lookupUserIsAdmin (UserId 42)) fixture () shouldBe True

it "returns False when the UserId corresponds to a non-admin user" $do let fixture = Fixture { _lookupUser = return$ Just User { isAdmin = False } }
unTestFixture (lookupUserIsAdmin (UserId 42)) fixture () shouldBe False

it "returns False when the UserId does not have a corresponding User" $do let fixture = Fixture { _lookupUser = return Nothing } unTestFixture (lookupUserIsAdmin (UserId 42)) fixture () shouldBe False As a final note, writing out all of these fixture record definitions and instance declarations can be extremely tedious with large numbers of typeclasses and tests. To mitigate this, the Control.Monad.TestFixture.TH module provides a mkFixture function, which uses Template Haskell to generate the necessary code instead. Synopsis # The TestFixture monad type TestFixture r w s = TestFixtureT r w s Identity Source # The TestFixture monad. A wrapper around the RWS monad, where the reader is a reified typeclass dictionary. For more information, see the module documentation for Control.Monad.TestFixture. type WS w s = WST w s Identity Source # A type alias for RWS where the reader component is always (). Used because the actual reader component is already occupied by the dictionary being threaded by the TestFixture monad. Arguments  :: TestFixture r () s a the monadic computation to run -> r (WS () s) the fixture dictionary to use -> s the initial monad state -> a the computation’s result The simplest way to run a test given a fixture, unTestFixture simply runs a monadic computation with a particular fixture and a starting state and returns the computations result. Useful for testing impure functions that return useful values. logTestFixture :: TestFixture r w s a -> r (WS w s) -> s -> w Source # Like unTestFixture, but instead of returning the result of the computation, logTestFixture returns the value written from the writer monad. Useful for testing impure functions called exclusively for side-effects that do not depend on complex prior state. evalTestFixture :: TestFixture r w s a -> r (WS w s) -> s -> (a, w) Source # Combines unTestFixture and logTestFixture to return both the computation’s result and the written value as a tuple. execTestFixture :: TestFixture r w s a -> r (WS w s) -> s -> (s, w) Source # Like logTestFixture but returns the final monadic state as well as the value written from the writer monad. Useful for testing stateful side-effectful computations. runTestFixture :: TestFixture r w s a -> r (WS w s) -> s -> (a, s, w) Source # Runs a test fixture and returns all three pieces of resulting information: the computation’s result, the final monadic state, and the value written from the writer. # The TestFixtureT monad transformer type TestFixtureT r w s m = ReaderT (r (WST w s m)) (WST w s m) Source # TestFixture as a monad transformer instead of as a monad. A wrapper around the RWST monad transformer. type WST w s m = RWST () w s m Source # The WS type alias equivalent for the TestFixtureT monad transformer. unTestFixtureT :: Monad m => TestFixtureT r () s m a -> r (WST () s m) -> s -> m a Source # The transformer equivalent of unTestFixture. logTestFixtureT :: Monad m => TestFixtureT r w s m a -> r (WST w s m) -> s -> m w Source # The transformer equivalent of logTestFixture. evalTestFixtureT :: Monad m => TestFixtureT r w s m a -> r (WST w s m) -> s -> m (a, w) Source # The transformer equivalent of evalTestFixture. execTestFixtureT :: Monad m => TestFixtureT r w s m a -> r (WST w s m) -> s -> m (s, w) Source # The transformer equivalent of execTestFixture. runTestFixtureT :: Monad m => TestFixtureT r w s m a -> r (WST w s m) -> s -> m (a, s, w) Source # The transformer equivalent of runTestFixture. # Helper functions arg0 :: Monoid w => (r (WS w s) -> WS w s a) -> TestFixture r w s a Source # A helper function for implementing typeclass instances over TestFixture that pull a value out of a monadic dictionary. For example, given the following instance: instance Monoid w => MonadSomething (TestFixture Fixture w s) where getSomething = do something <- asks _getSomething lift something Using arg0, it can be rewritten like this: instance Monoid w => MonadSomething (TestFixture Fixture w s) where getSomething = arg0 _getSomething For functions of various arities instead of plain values, use arg1 through arg7, instead. arg1 :: Monoid w => (r (WS w s) -> a -> WS w s b) -> a -> TestFixture r w s b Source # Like arg0, but for lifting record accessors containing functions of arity one. For example, given the following instance: instance Monoid w => MonadSomething (TestFixture Fixture w s) where doSomething x = do fn <- asks _doSomething lift$ fn x

Using arg1, it can be rewritten like this:

instance Monoid w => MonadSomething (TestFixture Fixture w s) where
doSomething = arg1 _doSomething

For functions of higher arities, use arg2 through arg7.

arg2 :: Monoid w => (r (WS w s) -> a -> b -> WS w s c) -> a -> b -> TestFixture r w s c Source #

Like arg1, but for functions of arity 2.

arg3 :: Monoid w => (r (WS w s) -> a -> b -> c -> WS w s d) -> a -> b -> c -> TestFixture r w s d Source #

Like arg1, but for functions of arity 3.

arg4 :: Monoid w => (r (WS w s) -> a -> b -> c -> d -> WS w s e) -> a -> b -> c -> d -> TestFixture r w s e Source #

Like arg1, but for functions of arity 4.

arg5 :: Monoid w => (r (WS w s) -> a -> b -> c -> d -> e -> WS w s f) -> a -> b -> c -> d -> e -> TestFixture r w s f Source #

Like arg1, but for functions of arity 5.

arg6 :: Monoid w => (r (WS w s) -> a -> b -> c -> d -> e -> f -> WS w s g) -> a -> b -> c -> d -> e -> f -> TestFixture r w s g Source #

Like arg1, but for functions of arity 6.

arg7 :: Monoid w => (r (WS w s) -> a -> b -> c -> d -> e -> f -> g -> WS w s h) -> a -> b -> c -> d -> e -> f -> g -> TestFixture r w s h Source #

Like arg1, but for functions of arity 7.

An extremely simple helper function for creating “base” fixture dictionaries with implementations that will simply throw as soon as they are called using a helpful error message. The provided argument should be the name of a method being implemented.

>>> unimplemented "_getSomething"
*** Exception: unimplemented fixture method _getSomething