packages feed

typeable-mock-0.1.0.1: test/Spec.hs

{-# 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