redact-0.6.0.0: test/Test/HMock/MockT.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
-- | This module defines monads for working with mocks. HMock tests run in the
-- 'MockT' monad transformer. A more limited monad, 'MockSetup', is used for
-- setting up defaults for each class. Both are instances of the 'MockContext'
-- monad, which defines a shared API.
module Test.HMock.MockT
( MockT,
runMockT,
withMockT,
nestMockT,
withNestedMockT,
Severity (..),
setAmbiguityCheck,
setUninterestingActionCheck,
setUnexpectedActionCheck,
setUnmetExpectationCheck,
describeExpectations,
verifyExpectations,
MockSetup,
MockContext,
allowUnexpected,
byDefault,
whenever,
)
where
import Control.Monad (join)
import Control.Monad.Reader
( MonadReader (..),
runReaderT,
)
import Control.Monad.Trans (lift)
import Data.List (intercalate)
import Data.Maybe (listToMaybe)
import Data.Proxy (Proxy (Proxy))
import GHC.Stack (callStack)
import Test.HMock.ExpectContext (MockableMethod)
import Test.HMock.Internal.ExpectSet
import Test.HMock.Internal.Rule (Rule ((:=>)))
import Test.HMock.Internal.State
import Test.HMock.Internal.Step (SingleRule ((:->)), Step (Step))
import Test.HMock.Internal.Util (locate)
import Test.HMock.Rule (Expectable (toRule))
import UnliftIO
-- | Runs a test in the 'MockT' monad, handling all of the mocks.
runMockT :: forall m a. MonadIO m => MockT m a -> m a
runMockT test = withMockT constTest
where
constTest :: (forall b. MockT m b -> m b) -> MockT m a
constTest _inMockT = test
-- | Runs a test in the 'MockT' monad. The test can unlift other MockT pieces
-- to the base monad while still acting on the same set of expectations. This
-- can be useful for testing concurrency or similar mechanisms.
--
-- @
-- test = 'withMockT' '$' \inMockT -> do
-- 'Test.HMock.Expectable.expect' '$' ...
--
-- 'liftIO' '$' 'Control.Concurrent.forkIO' '$' inMockT firstThread
-- 'liftIO' '$' 'Control.Concurrent.forkIO' '$' inMockT secondThread
-- @
--
-- This is a low-level primitive. Consider using the @unliftio@ package for
-- higher level implementations of multithreading and other primitives.
withMockT ::
forall m b. MonadIO m => ((forall a. MockT m a -> m a) -> MockT m b) -> m b
withMockT test = do
state <- initMockState Nothing
let inMockT :: forall a. MockT m a -> m a
inMockT m = runReaderT (unMockT m) state
flip runReaderT state $
unMockT $ do
a <- test inMockT
verifyExpectations
return a
-- | Starts a nested block within 'MockT'. The nested block has its own set of
-- expectations, which must be fulfilled before the end of the block.
--
-- Beware: use of 'nestMockT' might signify that you are doing too much in a
-- single test. Consider splitting large tests into a separate test for each
-- case.
nestMockT :: forall m a. MonadIO m => MockT m a -> MockT m a
nestMockT nest = withNestedMockT constNest
where
constNest :: (forall b. MockT m b -> m b) -> MockT m a
constNest _inMockT = nest
-- | Starts a nested block within 'MockT'. The nested block has its own set of
-- expectations, which must be fulfilled before the end of the block. It can
-- unlift other MockT pieces to the base monad while still acting on the same
-- set of expectations. This can be useful for testing concurrency or similar
-- mechanisms.
--
-- Beware: use of 'nestMockT' might signify that you are doing too much in a
-- single test. Consider splitting large tests into a separate test for each
-- case.
withNestedMockT ::
forall m b.
MonadIO m =>
((forall a. MockT m a -> m a) -> MockT m b) ->
MockT m b
withNestedMockT nest = do
parent <- MockT ask
state <- lift $ initMockState (Just parent)
withState state $ do
a <- nest (flip runReaderT state . unMockT)
verifyExpectations
return a
where
withState state = MockT . local (const state) . unMockT
-- | Sets the severity for ambiguous actions. An ambiguous action is one that
-- matches expectations in more than one way. If this is not set to `Error`,
-- the most recently added expectation will take precedence.
--
-- This defaults to 'Ignore'.
setAmbiguityCheck :: MonadIO m => Severity -> MockT m ()
setAmbiguityCheck severity = fromMockSetup $ do
state <- MockSetup ask
mockSetupSTM $ writeTVar (mockAmbiguitySeverity state) severity
-- | Sets the severity for uninteresting actions. An uninteresting action is
-- one for which no expectations or other configuration have been added that
-- mention the method at all. If this is not set to `Error`, then uninteresting
-- methods are treated just like unexpected methods.
--
-- Before you weaken this check, consider that the labeling of methods as
-- "uninteresting" is non-compositional. A change in one part of your test can
-- result in a formerly uninteresting action being considered interesting in a
-- different part of the test.
--
-- This defaults to 'Error'.
setUninterestingActionCheck :: MonadIO m => Severity -> MockT m ()
setUninterestingActionCheck severity = fromMockSetup $ do
state <- MockSetup ask
mockSetupSTM $ writeTVar (mockUninterestingSeverity state) severity
-- | Sets the severity for unexpected actions. An unexpected action is one that
-- doesn't match any expectations *and* isn't explicitly allowed by
-- `allowUnexpected`. If this is not set to `Error`, the action returns its
-- default response.
--
-- This defaults to 'Error'.
setUnexpectedActionCheck :: MonadIO m => Severity -> MockT m ()
setUnexpectedActionCheck severity = fromMockSetup $ do
state <- MockSetup ask
mockSetupSTM $ writeTVar (mockUnexpectedSeverity state) severity
-- | Sets the severity for unmet expectations. An unmet expectation happens
-- when an expectation is added, but either the test (or nesting level) ends or
-- 'verifyExpectations' is used before a matching action takes place.
--
-- This defaults to 'Error'.
setUnmetExpectationCheck :: MonadIO m => Severity -> MockT m ()
setUnmetExpectationCheck severity = fromMockSetup $ do
state <- MockSetup ask
mockSetupSTM $ writeTVar (mockUnmetSeverity state) severity
-- | Fetches a 'String' that describes the current set of outstanding
-- expectations. This is sometimes useful for debugging test code. The exact
-- format is not specified.
describeExpectations :: MonadIO m => MockT m String
describeExpectations = fromMockSetup $ do
states <- allStates <$> MockSetup ask
expectSets <- mapM (mockSetupSTM . readTVar . mockExpectSet) states
return $
intercalate "\n----- (next layer) -----\n" $
formatExpectSet <$> expectSets
-- | Verifies that all mock expectations are satisfied. If there is a nested
-- block in effect, only the expectations of that nested block are verified
-- You normally don't need to do this, because it happens automatically at the
-- end of your test or nested block. However, it's occasionally useful to check
-- expectations early.
--
-- Beware: use of 'verifyExpectations' might signify that you are doing too much
-- in a single test. Consider splitting large tests into a separate test for
-- each case.
verifyExpectations :: MonadIO m => MockT m ()
verifyExpectations = join $ do
fromMockSetup $ do
states <- MockSetup ask
expectSet <- mockSetupSTM $ readTVar $ mockExpectSet states
missingSev <- mockSetupSTM $ readTVar $ mockUnmetSeverity states
case excess expectSet of
ExpectNothing -> return (return ())
missing ->
return $
reportFault missingSev $
"Unmet expectations:\n" ++ formatExpectSet missing
-- | Adds a handler for unexpected actions. Matching calls will not fail, but
-- will use a default response instead. The rule passed in must have zero or
-- one responses: if there is a response, @'allowUnexpected' (m
-- 'Test.HMock.Rule.|=>' r)@ is equivalent to @'allowUnexpected' m >>
-- 'byDefault' (m 'Test.HMock.Rule.|=>' r)@.
--
-- The difference between 'Test.HMock.Expectable.expectAny' and
-- 'allowUnexpected' is subtle, but comes down to ambiguity:
--
-- * 'allowUnexpected' is not an expectation, so it cannot be ambiguous. It
-- only has an effect if no true expectation matches, regardless of when the
-- expectations were added.
-- * 'Test.HMock.Expectable.expectAny' adds an expectation, so if another
-- expectation is in effect at the same time, a call to the method is
-- ambiguous. If ambiguity checking is enabled, the method will throw an
-- error; otherwise, the more recently added of the two expectations is used.
allowUnexpected ::
forall cls name m r rule ctx.
( MonadIO m,
MockableMethod cls name m r,
Expectable cls name m r rule,
MockContext ctx
) =>
rule ->
ctx m ()
allowUnexpected e = fromMockSetup $ case toRule e of
_ :=> (_ : _ : _) -> error "allowUnexpected may not have multiple responses."
m :=> r -> do
initClassIfNeeded (Proxy :: Proxy cls)
state <- MockSetup ask
mockSetupSTM $
modifyTVar'
(mockAllowUnexpected state)
(Step (locate callStack (m :-> listToMaybe r)) :)
-- | Sets a default action for *expected* matching calls. The new default only
-- applies to calls for which an expectation exists, but it lacks an explicit
-- response. The rule passed in must have exactly one response.
byDefault ::
forall cls name m r ctx.
( MonadIO m,
MockableMethod cls name m r,
MockContext ctx
) =>
Rule cls name m r ->
ctx m ()
byDefault (m :=> [r]) = fromMockSetup $ do
initClassIfNeeded (Proxy :: Proxy cls)
state <- MockSetup ask
mockSetupSTM $
modifyTVar'
(mockDefaults state)
(Step (locate callStack (m :-> Just r)) :)
byDefault _ = error "Defaults must have exactly one response."
-- | Adds a side-effect, which happens whenever a matching call occurs, in
-- addition to the usual response. The return value is entirely ignored.
--
-- Be warned: using side effects makes it easy to break abstraction boundaries.
-- Be aware that there may be other uses of a method besides the one which you
-- intend to intercept here. If possible, add the desired behavior to the
-- response for the matching expectation instead.
whenever ::
forall cls name m r ctx.
( MonadIO m,
MockableMethod cls name m r,
MockContext ctx
) =>
Rule cls name m r ->
ctx m ()
whenever (m :=> [r]) = fromMockSetup $ do
initClassIfNeeded (Proxy :: Proxy cls)
state <- MockSetup ask
mockSetupSTM $
modifyTVar'
(mockSideEffects state)
(Step (locate callStack (m :-> Just r)) :)
whenever _ = error "Side effects must have exactly one response."