{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE RankNTypes #-} -- | This module defines the 'ExpectContext' class, whose members provide the -- combinators for building the execution plan for your mocks. Notably, there -- is a 'Test.HMock.MockT.MockT' instance for 'ExpectContext', so you can use -- these combinators to add expectations inside your tests that run in -- 'Test.HMock.MockT.MockT', as well as nesting them in other combinators. module Test.HMock.ExpectContext ( MockableMethod, ExpectContext (..), ) where import Control.Monad.IO.Class (MonadIO) import Data.Kind (Constraint, Type) import Data.Typeable (Typeable) import GHC.Stack (HasCallStack) import GHC.TypeLits (KnownSymbol, Symbol) import Test.HMock.Mockable (Mockable) import Test.HMock.Multiplicity (Multiplicity) import Test.HMock.Rule (Expectable) -- | All constraints needed to mock a method with the given class, name, base -- monad, and return type. type MockableMethod (cls :: (Type -> Type) -> Constraint) (name :: Symbol) (m :: Type -> Type) (r :: Type) = (Mockable cls, Typeable m, KnownSymbol name, Typeable r) -- | Type class for contexts in which one can build expectations. Notably, this -- includes `Test.HMock.MockT.MockT`, which expects actions to be performed -- during a test. -- -- The methods of this class represent the user-facing API for build your -- execution plan for mocks. class ExpectContext (ctx :: (Type -> Type) -> Type -> Type) where -- | Creates an expectation that an action is performed once per given -- response (or exactly once if there is no response). -- -- @ -- 'Test.HMock.MockT.runMockT' '$' do -- 'expect' '$' -- ReadFile "foo.txt" -- 'Test.HMock.Rule.|->' "lorem ipsum" -- 'Test.HMock.Rule.|->' "oops, the file changed out from under me!" -- callCodeUnderTest -- @ -- -- In this example, `readFile` must be called exactly twice by the tested -- code, and will return "lorem ipsum" the first time, but something different -- the second time. expect :: ( HasCallStack, MonadIO m, MockableMethod cls name m r, Expectable cls name m r expectable ) => expectable -> ctx m () -- | Creates an expectation that an action is performed some number of times. -- -- @ -- 'Test.HMock.MockT.runMockT' '$' do -- 'expect' '$' MakeList -- 'expectN' ('Test.HMock.atLeast' 2) '$' -- CheckList "Cindy Lou Who" 'Test.HMock.Rule.|->' Nice -- -- callCodeUnderTest -- @ expectN :: ( HasCallStack, MonadIO m, MockableMethod cls name m r, Expectable cls name m r expectable ) => -- | The number of times the action should be performed. Multiplicity -> -- | The action and its response. expectable -> ctx m () -- | Specifies a response if a matching action is performed, but doesn't -- expect anything. This is equivalent to @'expectN' -- 'Test.HMock.Multiplicity.anyMultiplicity'@, but shorter. -- -- In this example, the later use of 'expectAny' overrides earlier uses, but -- only for calls that match its conditions. -- -- @ -- 'Test.HMock.MockT.runMockT' '$' do -- 'expectAny' '$' -- ReadFile_ anything 'Test.HMock.Rule.|->' "tlhIngan maH!" -- 'expectAny' '$' -- ReadFile "config.txt" 'Test.HMock.Rule.|->' "lang: klingon" -- -- callCodeUnderTest -- @ expectAny :: ( HasCallStack, MonadIO m, MockableMethod cls name m r, Expectable cls name m r expectable ) => expectable -> ctx m () -- | Creates a sequential expectation. Other actions can still happen during -- the sequence, but these specific expectations must be met in this order. -- -- @ -- 'inSequence' -- [ 'expect' '$' MoveForward, -- 'expect' '$' TurnRight, -- 'expect' '$' MoveForward -- ] -- @ -- -- Beware of using 'inSequence' too often. It is appropriate when the -- property you are testing is that the order of effects is correct. If -- that's not the purpose of the test, consider adding several independent -- expectations, instead. This avoids over-asserting, and keeps your tests -- less brittle. inSequence :: MonadIO m => (forall ctx'. ExpectContext ctx' => [ctx' m ()]) -> ctx m () -- | Combines multiple expectations, which can occur in any order. Most of -- the time, you can achieve the same thing by expecting each separately, but -- this can be combined in compound expectations to describe more complex -- ordering constraints. -- -- If ambiguity checking is disabled, the choice is left-biased, so earlier -- options are preferred over ambiguous later options. -- -- @ -- 'inSequence' -- [ 'inAnyOrder' -- [ 'expect' '$' AdjustMirrors, -- 'expect' '$' FastenSeatBelt -- ], -- 'expect' '$' StartCar -- ] -- @ inAnyOrder :: MonadIO m => (forall ctx'. ExpectContext ctx' => [ctx' m ()]) -> ctx m () -- | Combines multiple expectations, requiring exactly one of them to occur. -- If ambiguity checking is disabled, the choice is left-biased, so earlier -- options are preferred over ambiguous later options. -- -- @ -- 'anyOf' -- [ 'expect' $ ApplyForJob, -- 'expect' $ ApplyForUniversity -- ] -- @ anyOf :: MonadIO m => (forall ctx'. ExpectContext ctx' => [ctx' m ()]) -> ctx m () -- | Creates a parent expectation that the child expectation will happen a -- certain number of times. Unlike `expectN`, the child expectation can be -- arbitrarily complex and span multiple actions. Also unlike 'expectN', each -- new execution will restart response sequences for rules with more than one -- response. -- -- Different occurrences of the child can be interleaved. If ambiguity -- checking is disabled, progressing on an existing occurrence is preferred -- over starting a new occurrence when it's ambiguous. times :: MonadIO m => Multiplicity -> (forall ctx'. ExpectContext ctx' => ctx' m ()) -> ctx m () -- | Creates a parent expectation that the child expectation will happen a -- certain number of times. Unlike `expectN`, the child expectation can be -- arbitrarily complex and span multiple actions. Also unlike 'expectN', each -- new execution will restart response sequences for rules with more than one -- response. -- -- Different occurrences of the child must happen consecutively, with one -- finishing before the next begins. consecutiveTimes :: MonadIO m => Multiplicity -> (forall ctx'. ExpectContext ctx' => ctx' m ()) -> ctx m ()