{-# LANGUAGE TypeApplications #-}
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO (..))
import Data.List (isInfixOf)
import Data.Maybe (fromMaybe, isNothing)
import Data.Typeable (Typeable)
import Test.Hspec
import Test.TypeableMock
main :: IO ()
main = hspec $ do
let withMock :: Typeable f => MockConfig -> String -> f -> f
withMock conf key f = fromMaybe f (lookupMockFunction conf key)
printWithMock :: (Show a, Typeable a) => MockConfig -> a -> IO ()
printWithMock conf = withMock conf "print" print
printStringMock <- runIO $ makeMock "print" (const $ pure () :: String -> IO ())
let mockConf :: MockConfig
mockConf = defaultMockConfig `addMocksToConfig` [printStringMock]
before_ (resetAllCallRecords mockConf) $ do
describe "Assertions" $ do
describe "expectCall" $ do
it "mocks a single argument function" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall "one"] printStringMock
it "mocks a multiple arguments function" $ do
let print2 :: Int -> Int -> IO ()
print2 a b = print (a, b)
print2WithMock conf = withMock conf "print2" print2 1 2
print2Mock <- makeMock "print2" ((\_ _ -> pure ()) :: Int -> Int -> IO ())
print2WithMock $ defaultMockConfig `addMocksToConfig` [print2Mock]
assertHasCalls [expectCall (1 :: Int) (2 :: Int)] print2Mock
it "mocks polymorphic monads with MockMonadIO" $ do
-- Polymorphic types cannot be used with Typeable typeOf. This library has a workaround for monads.
let print5 a b c d e = liftIO $ print (a, b, c, d, e)
let printInMonadIO :: forall m. MonadIO m => MockConfig -> Bool -> Char -> Char -> Char -> Char -> m ()
printInMonadIO conf =
maybe print5 unMockMonadIO5 (lookupMockFunction conf "print5")
let printInIO :: MockConfig -> Bool -> Char -> Char -> Char -> Char -> IO ()
printInIO conf =
maybe print5 fromMockMonadIO (lookupMockFunction conf "print5")
print5Mock <- makeMock "print5" (constN $ pure () :: Bool -> Char -> Char -> Char -> Char -> MockMonadIO ())
let mockConf' :: MockConfig
mockConf' = mockConf `addMocksToConfig` [print5Mock]
printInMonadIO mockConf' True 'a' 'b' 'c' 'd'
printInIO mockConf' True 'a' 'b' 'c' 'd'
assertHasCalls [expectCall True 'a' 'b' 'c' 'd', expectCall True 'a' 'b' 'c' 'd'] print5Mock
describe "assertHasCalls" $ do
it "mocks multiple calls" $ do
printWithMock mockConf "one"
printWithMock mockConf "two"
assertHasCalls
[ expectCall "one",
expectCall "two"
]
printStringMock
it "fails when there are more calls than expected" $
do
printWithMock mockConf "one"
printWithMock mockConf "two"
assertHasCalls [expectCall "one"] printStringMock
`shouldThrow` \case
MockFailure {mfReason = MockFailureUnexpectedCall {}} -> True
_ -> False
it "fails when there are fewer calls than expected" $ do
printWithMock mockConf "one"
assertHasCalls
[ expectCall "one",
expectCall "two"
]
printStringMock
`shouldThrow` \case
MockFailure {mfReason = MockFailureNotCalled {}} -> True
_ -> False
it "fails when a call is with different arguments" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall "two"] printStringMock `shouldThrow` \case
MockFailure {mfReason = MockFailureArgumentValueMismatch {}} -> True
_ -> False
it "fails when a call returns different result" $ do
let doubleFunc :: Int -> IO Int
doubleFunc a = print a >> pure (a * 2)
doubleMock <- makeMock "double" ((\a -> pure (a * 2)) `asTypeOf` doubleFunc)
let mockConf' = mockConf `addMocksToConfig` [doubleMock]
void $ withMock mockConf' "double" doubleFunc 5
assertHasCalls [expectCall AnyVal `withResult` (10 :: Int)] doubleMock
assertHasCalls [expectCall AnyVal `withResult` (11 :: Int)] doubleMock
`shouldThrow` \case
MockFailure {mfReason = MockFailureArgumentValueMismatch {}} -> True
_ -> False
describe "assertNotCalled" $ do
it "succeeds when mock was not called" $ do
assertNotCalled printStringMock
it "fails when mock was called" $ do
printWithMock mockConf "one"
assertNotCalled printStringMock `shouldThrow` \case
MockFailure {mfReason = MockFailureUnexpectedCall {}} -> True
_ -> False
describe "assertNotCalled" $ do
it "fails when mock was not called" $ do
printWithMock mockConf "two"
assertAnyCall (expectCall "one") printStringMock
`shouldThrow` \case
MockFailure {mfReason = MockFailureNotCalled {}} -> True
_ -> False
describe "Config" $ do
it "addMocksToConfig overrides mocks" $ do
printIntMock1 <- makeMock "print" (const $ pure () :: Int -> IO ())
printIntMock2 <- makeMock "print" (const $ pure () :: Int -> IO ())
let mockConf' = mockConf `addMocksToConfig` [printIntMock1, printIntMock2]
printWithMock mockConf' (1 :: Int)
assertHasCalls [expectCall (1 :: Int)] printIntMock2
printIntMock3 <- makeMock "print" (const $ pure () :: Int -> IO ())
let mockConf'' = mockConf `addMocksToConfig` [printIntMock3]
printWithMock mockConf'' (1 :: Int)
assertHasCalls [expectCall (1 :: Int)] printIntMock3
describe "Arguments comparison" $ do
it "checks argument count" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall "a" "b"] printStringMock `shouldThrow` \case
MockFailure {mfReason = MockFailureArgumentCountMismatch {}} -> True
_ -> False
it "checks argument types" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall ()] printStringMock `shouldThrow` \case
MockFailure {mfReason = MockFailureArgumentTypeMismatch {}} -> True
_ -> False
it "checks predicate for argument" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall (PredicateVal (== "one"))] printStringMock
assertHasCalls [expectCall (PredicateVal (== "two"))] printStringMock
`shouldThrow` \case
MockFailure {mfReason = MockFailureArgumentPredicateFailure {}} -> True
_ -> False
it "ignores expected argument when it is AnyVal" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall AnyVal] printStringMock
it "callMatches fails on incompatible arguments" $ do
(callMatches (ActualCallRecord [] (ActualVal ())) (ExpectedCallRecord [] (ExpectedVal True)) `seq` pure ())
`shouldThrow` anyErrorCall
describe "Retrieving mocks" $ do
describe "lookupMock" $ do
it "finds mock" $ do
show (lookupMock mockConf "print") `shouldBe` show printStringMock
it "finds mocks with different types" $ do
printIntMock <- makeMock "print" (const $ pure () :: Int -> IO ())
let mockConf' = mockConf `addMocksToConfig` [printIntMock]
printWithMock mockConf' "one"
printWithMock mockConf' (1 :: Int)
assertHasCalls [expectCall "one"] printStringMock
assertHasCalls [expectCall (1 :: Int)] printIntMock
it "fails for multiple mocks with the same name" $ do
printIntMock <- makeMock "print" ((\_ -> pure ()) :: Int -> IO ())
let mockConf' = mockConf `addMocksToConfig` [printIntMock]
(lookupMock mockConf' "print" `seq` pure ())
`shouldThrow` errorCall "lookupMock: There are 2 mocks under the name \"print\". Use lookupMockTyped to disambiguate."
it "fails when there is no mock" $ do
(lookupMock mockConf "noSuchMock" `seq` pure ())
`shouldThrow` errorCall "lookupMock: Mock noSuchMock not found"
describe "lookupMockTyped" $ do
it "finds mock" $ do
printIntMock <- makeMock "print" ((\_ -> pure ()) :: Int -> IO ())
let mockConf' = mockConf `addMocksToConfig` [printIntMock]
let Just mock = lookupMockTyped @(Int -> IO ()) mockConf' "print"
show mock `shouldBe` show printIntMock
it "fails when mcShouldFailOnNotFound and there are no mocks with the given name " $ do
let mockConf' = mockConf {mcShouldFailOnNotFound = \_ _ -> True}
withMock mockConf' "printDoesNotExist" print ()
`shouldThrow` errorCall "lookupMockTyped: cannot find mock printDoesNotExist :: () -> IO (). There are no mocks under this name."
it "fails when mcShouldFailOnNotFound and there are other mocks with the same name" $ do
let mockConf' = mockConf {mcShouldFailOnNotFound = \_ _ -> True}
(lookupMockTyped @(Int -> IO ()) mockConf' "print" `seq` pure ())
`shouldThrow` errorCall "lookupMockTyped: cannot find mock print :: Int -> IO (). There are mocks with other types under the same name:\nMock (print :: [Char] -> IO ())\n"
it "returns Nothing when there are no mocks with the given name" $ do
lookupMockTyped @(Int -> IO ()) mockConf "noSuchMock"
`shouldSatisfy` isNothing
it "returns Nothing when there are other mocks with the same name" $ do
lookupMockTyped @(Int -> IO ()) mockConf "print"
`shouldSatisfy` isNothing
describe "Messages" $ do
it "shows Mock" $ do
mock <- makeMock "print" (const $ pure () :: Int -> IO ())
show mock `shouldBe` "Mock (print :: Int -> IO ())"
it "shows MockFailure" $ do
printWithMock mockConf "one"
assertHasCalls [expectCall "two"] printStringMock `shouldThrow` \(e :: MockFailure) ->
"test/Spec.hs" `isInfixOf` show e