packages feed

HMock-0.4.0.0: test/Core.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Core where

import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)
import Control.DeepSeq (rnf)
import Control.Exception (SomeException, evaluate)
import Control.Monad (replicateM_)
import Control.Monad.Reader (MonadReader (local), ask, runReaderT)
import Control.Monad.State (execStateT, modify)
import Control.Monad.Trans (liftIO)
import Data.IORef
  ( modifyIORef,
    newIORef,
    readIORef,
    writeIORef,
  )
import Data.List (isInfixOf, isPrefixOf)
import Test.HMock
import Test.Hspec
import qualified UnliftIO.Concurrent as UnliftIO
import Prelude hiding (readFile, writeFile)
import qualified Prelude

class Monad m => MonadFilesystem m where
  readFile :: FilePath -> m String
  writeFile :: FilePath -> String -> m ()

-- | This is not used by tests.  It's just an illustration of how you'd use
-- 'MonadFilesystem' in production.
instance MonadFilesystem IO where
  readFile = Prelude.readFile
  writeFile = Prelude.writeFile

makeMockable [t|MonadFilesystem|]

newtype SocketHandle = Handle Int deriving (Eq, Show)

class Monad m => MonadSocket m where
  openSocket :: Int -> m SocketHandle
  closeSocket :: SocketHandle -> m ()

makeMockable [t|MonadSocket|]

coreTests :: SpecWith ()
coreTests = do
  describe "HMock core" $ do
    it "verifies a file copy" $
      example $ do
        let copyFile :: MonadFilesystem m => FilePath -> FilePath -> m ()
            copyFile a b = readFile a >>= writeFile b

        runMockT $ do
          expect $ ReadFile "foo.txt" |-> "lorem ipsum"
          expect $ WriteFile "bar.txt" "lorem ipsum"

          copyFile "foo.txt" "bar.txt"

    it "rejects an incorrect file copy" $
      example $ do
        let badCopyFile :: MonadFilesystem m => FilePath -> FilePath -> m ()
            badCopyFile a b = readFile b >>= writeFile a

            failure = runMockT $ do
              expect $ ReadFile "foo.txt" |-> "lorem ipsum"
              expect $ WriteFile "bar.txt" "lorem ipsum"

              badCopyFile "foo.txt" "bar.txt"

        failure `shouldThrow` anyException

    it "uses default responses when no explicit response given" $
      example $ do
        let test = runMockT $ do
              expect $ WriteFile "file.txt" "contents"
              expect $ ReadFile "file.txt"
              writeFile "file.txt" "contents"
              readFile "file.txt"

        test `shouldReturn` ""

    it "shares expectations using withMockT" $
      example $
        withMockT $ \inMockT -> do
          expect $ ReadFile "foo.txt" |-> "lorem ipsum"
          expect $ WriteFile "bar.txt" "lorem ipsum"

          var <- liftIO newEmptyMVar

          _ <-
            liftIO $
              forkIO $ inMockT $ readFile "foo.txt" >>= liftIO . putMVar var
          writeFile "bar.txt" =<< liftIO (takeMVar var)

    it "shares expectations across threads using MonadUnliftIO" $
      example $
        runMockT $ do
          expect $ ReadFile "foo.txt" |-> "lorem ipsum"
          expect $ WriteFile "bar.txt" "lorem ipsum"

          var <- liftIO newEmptyMVar
          _ <- UnliftIO.forkIO $ readFile "foo.txt" >>= liftIO . putMVar var
          writeFile "bar.txt" =<< liftIO (takeMVar var)

    it "tracks expectations across multiple classes" $
      example $ do
        let setExpectations =
              inSequence
                [ expect $ ReadFile "code.txt" |-> "alpha",
                  expect $ OpenSocket 80 |-> Handle 80,
                  expect $ WriteFile "code.txt" "alpha+",
                  expect $ CloseSocket (Handle 80)
                ]

            success = runMockT $ do
              setExpectations

              code <- readFile "code.txt"
              h <- openSocket 80
              writeFile "code.txt" (code ++ "+")
              closeSocket h

            failure = runMockT $ do
              setExpectations

              h <- openSocket 80
              code <- readFile "code.txt"
              closeSocket h
              writeFile "code.txt" (code ++ "+")

        success

        failure
          `shouldThrow` errorWith ("Unexpected action: openSocket" `isInfixOf`)

    it "returns multiple responses" $
      example $ do
        let test = runMockT $ do
              expect $
                ReadFile "foo.txt"
                  |-> "a"
                  |-> "b"
                  |-> "c"
              (,,)
                <$> readFile "foo.txt"
                <*> readFile "foo.txt"
                <*> readFile "foo.txt"

        test `shouldReturn` ("a", "b", "c")

    it "catches expectN with too many expectations" $
      example $ do
        let test = runMockT $ do
              expectN once $
                ReadFile "foo.txt"
                  |-> "a"
                  |-> "b"

        test `shouldThrow` errorWith ("2 responses is too many" `isInfixOf`)

    it "catches unmet expectations" $
      example $ do
        let test = runMockT $ do
              expect $ WriteFile "bar.txt" "bar"

              -- Don't write the file.
              return ()

        test
          `shouldThrow` errorWith
            (("Unmet expectations" `isInfixOf`) <&&> ("Core.hs:" `isInfixOf`))

    it "catches partially unmet expectations" $
      example $ do
        let test = runMockT $ do
              expect $ WriteFile "foo.txt" "foo"
              expect $ WriteFile "bar.txt" "bar"

              writeFile "foo.txt" "foo"

        test `shouldThrow` errorWith ("Unmet expectations" `isInfixOf`)

    it "catches partially unmet sequences" $
      example $ do
        let test = runMockT $ do
              inSequence
                [ expect $ WriteFile "foo.txt" "foo",
                  expect $ WriteFile "bar.txt" "bar",
                  expect $ WriteFile "baz.txt" "baz"
                ]

              writeFile "foo.txt" "foo"

        test `shouldThrow` errorWith ("Unmet expectations" `isInfixOf`)

    it "catches unexpected actions" $
      example $
        runMockT (writeFile "bar.txt" "bar")
          `shouldThrow` errorWith ("Unexpected action: writeFile" `isInfixOf`)

    it "catches incorrect arguments" $
      example $ do
        let test = runMockT $ do
              expect $ WriteFile "bar.txt" "bar"
              writeFile "bar.txt" "incorrect"

        test
          `shouldThrow` errorWith
            (("Wrong arguments" `isInfixOf`) <&&> ("Core.hs:" `isInfixOf`))

    it "matches with imprecise predicates" $
      example . runMockT $ do
        expect $ WriteFile_ (hasSubstr "bar") anything
        writeFile "bar.txt" "unknown contents"

    it "matches with WholeMethodMatcher" $
      example $ do
        let setExpectations =
              expect $
                WriteFile_ anything anything
                  `SuchThat` \(WriteFile f txt) -> txt `isInfixOf` f

            success = runMockT $ do
              setExpectations
              writeFile "foo.txt" "foo"

            failure = runMockT $ do
              setExpectations
              writeFile "foo.txt" "bar"

        success
        failure `shouldThrow` anyException

    it "stores source location in suchThat predicate" $
      example $ do
        let test = runMockT $ do
              expect $ ReadFile_ (is ("foo" `isPrefixOf`)) |-> "foo"
              readFile "bar.txt"

        test `shouldThrow` errorWith ("Core.hs" `isInfixOf`)

    it "prefers most recent method match" $
      example . runMockT $ do
        expectAny $ ReadFile "foo.txt" |-> "a"
        expectAny $ ReadFile "foo.txt" |-> "b"
        expect $ ReadFile "foo.txt" |-> "c"
        expect $ ReadFile "foo.txt" |-> "d"

        r1 <- readFile "foo.txt"
        r2 <- readFile "foo.txt"
        r3 <- readFile "foo.txt"
        r4 <- readFile "foo.txt"

        liftIO $ r1 `shouldBe` "d"
        liftIO $ r2 `shouldBe` "c"
        liftIO $ r3 `shouldBe` "b"
        liftIO $ r4 `shouldBe` "b"

    it "matches flexible multiplicity" $
      example $ do
        let setExpectations = do
              expectN (atLeast 3) $ ReadFile "foo.txt" |-> "foo"
              expectN (atMost 2) $ ReadFile "bar.txt" |-> "bar"
              expectAny $ ReadFile "baz.txt" |-> "baz"

            success1 = runMockT $ do
              setExpectations
              replicateM_ 3 $ readFile "foo.txt"

            success2 = runMockT $ do
              setExpectations
              replicateM_ 4 $ readFile "foo.txt"

            success3 = runMockT $ do
              setExpectations
              replicateM_ 3 $ readFile "foo.txt"
              replicateM_ 2 $ readFile "bar.txt"

            success4 = runMockT $ do
              setExpectations
              replicateM_ 3 $ readFile "foo.txt"
              replicateM_ 2 $ readFile "bar.txt"
              replicateM_ 5 $ readFile "baz.txt"

            failure1 = runMockT $ do
              setExpectations
              replicateM_ 1 $ readFile "foo.txt"

            failure2 = runMockT $ do
              setExpectations
              replicateM_ 3 $ readFile "foo.txt"
              replicateM_ 3 $ readFile "bar.txt"

        success1
        success2
        success3
        success4
        failure1 `shouldThrow` anyException
        failure2 `shouldThrow` anyException

    it "describes argument predicates that don't match" $
      example $ do
        let test = runMockT $ do
              expect $ WriteFile_ (eq "foo.txt") (hasSubstr "foo")
              _ <- writeFile "foo.txt" "bar"
              return ()
        test
          `shouldThrow` errorWith
            ("Arg #2: \"bar\" doesn't have substring \"foo\"" `isInfixOf`)

    it "enforces nested sequences" $
      example $ do
        let setExpectations =
              inSequence
                [ inAnyOrder
                    [ expect $ ReadFile "1.txt" |-> "1",
                      expect $ ReadFile "2.txt" |-> "2"
                    ],
                  expect $ ReadFile "3.txt" |-> "3"
                ]

            success1 = runMockT $ do
              setExpectations
              _ <- readFile "1.txt"
              _ <- readFile "2.txt"
              _ <- readFile "3.txt"
              return ()

            success2 = runMockT $ do
              setExpectations
              _ <- readFile "2.txt"
              _ <- readFile "1.txt"
              _ <- readFile "3.txt"
              return ()

            failure = runMockT $ do
              setExpectations
              _ <- readFile "2.txt"
              _ <- readFile "3.txt"
              _ <- readFile "1.txt"
              return ()

        success1
        success2
        failure `shouldThrow` anyException

    it "handles nested sequences" $
      example . runMockT $ do
        inSequence
          [ inSequence
              [ expect $ ReadFile "a" |-> "a",
                expect $ ReadFile "b" |-> "b"
              ],
            expect $ ReadFile "c" |-> "c"
          ]

        _ <- readFile "a"
        _ <- readFile "b"
        _ <- readFile "c"
        return ()

    it "consumes optional calls in sequences" $
      example $ do
        let setExpectations =
              inSequence
                [ expectAny $ WriteFile "foo.txt" "foo",
                  expectAny $ WriteFile "foo.txt" "bar"
                ]

            success = runMockT $ do
              setExpectations
              writeFile "foo.txt" "foo"
              writeFile "foo.txt" "bar"

            failure = runMockT $ do
              setExpectations
              writeFile "foo.txt" "foo"
              writeFile "foo.txt" "bar"
              writeFile "foo.txt" "foo"

        success
        failure `shouldThrow` errorWith ("Wrong arguments:" `isInfixOf`)

    it "implements choice" $
      example $ do
        let setExpectations =
              anyOf
                [ expect $ WriteFile "status.txt" "all systems go",
                  expect $ WriteFile "status.txt" "we have a problem"
                ]

            success1 = runMockT $ do
              setExpectations
              writeFile "status.txt" "all systems go"

            success2 = runMockT $ do
              setExpectations
              writeFile "status.txt" "we have a problem"

            failure1 = runMockT $ do
              setExpectations
              return ()

            failure2 = runMockT $ do
              setExpectations
              writeFile "status.txt" "not sure"

        success1
        success2
        failure1 `shouldThrow` anyException
        failure2 `shouldThrow` anyException

    it "implements interleaved repetition" $
      example $ do
        let setExpectations =
              times (atLeast 2) $
                inAnyOrder
                  [ expect $ WriteFile "foo.txt" "a",
                    expect $ WriteFile "bar.txt" "b"
                  ]

            success1 = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

              writeFile "bar.txt" "b"
              writeFile "foo.txt" "a"

            success2 = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "foo.txt" "a"

              writeFile "bar.txt" "b"
              writeFile "bar.txt" "b"

            tooFew = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

            incomplete = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

              writeFile "foo.txt" "a"

        success1
        success2
        tooFew `shouldThrow` anyException
        incomplete `shouldThrow` anyException

    it "implements consecutive repetition" $
      example $ do
        let setExpectations =
              consecutiveTimes (atLeast 2) $
                inAnyOrder
                  [ expect $ WriteFile "foo.txt" "a",
                    expect $ WriteFile "bar.txt" "b"
                  ]

            success1 = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

              writeFile "bar.txt" "b"
              writeFile "foo.txt" "a"

            interleaved = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "foo.txt" "a"

              writeFile "bar.txt" "b"
              writeFile "bar.txt" "b"

            tooFew = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

            incomplete = runMockT $ do
              setExpectations

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

              writeFile "foo.txt" "a"
              writeFile "bar.txt" "b"

              writeFile "foo.txt" "a"

        success1
        interleaved `shouldThrow` anyException
        tooFew `shouldThrow` anyException
        incomplete `shouldThrow` anyException

    it "repeats response sequences during repetition" $
      example $ do
        runMockT $ do
          times 2 $ expect $ ReadFile "foo.txt" |-> "A" |-> "B"

          result <-
            (,,,) <$> readFile "foo.txt"
              <*> readFile "foo.txt"
              <*> readFile "foo.txt"
              <*> readFile "foo.txt"
          liftIO $ result `shouldBe` ("A", "B", "A", "B")

    it "repeats response sequences during consecutive repetition" $
      example $ do
        runMockT $ do
          consecutiveTimes 2 $ expect $ ReadFile "foo.txt" |-> "A" |-> "B"

          result <-
            (,,,) <$> readFile "foo.txt"
              <*> readFile "foo.txt"
              <*> readFile "foo.txt"
              <*> readFile "foo.txt"
          liftIO $ result `shouldBe` ("A", "B", "A", "B")

    it "gives access to method arguments in the response" $
      example $ do
        let test = runMockT $ do
              expect $
                ReadFile_ anything
                  |=> \(ReadFile f) -> return ("contents of " ++ f)

              readFile "foo.txt"

        test `shouldReturn` "contents of foo.txt"

    it "allows responses to run in the underlying monad" $
      example $ do
        ref <- newIORef ""
        runMockT $ do
          expectAny $
            WriteFile_ (eq "foo.txt") anything
              |=> \(WriteFile _ c) -> liftIO (writeIORef ref c)
          writeFile "foo.txt" "open sesame"
        readIORef ref `shouldReturn` "open sesame"

    it "respects expectations added by a response" $
      example $ do
        let setExpectations = do
              expectAny $
                OpenSocket_ anything |=> \(OpenSocket n) -> do
                  expect $ CloseSocket (Handle n)
                  return (Handle n)

            success = runMockT $ do
              setExpectations

              h <- openSocket 80
              closeSocket h

            failure = runMockT $ do
              setExpectations

              _ <- openSocket 80
              return ()

        success
        failure `shouldThrow` anyException

    it "has a correct implementation of MonadReader" $
      example $ do
        flip runReaderT "read me" $
          runMockT $ do
            expectAny $ ReadFile_ anything |=> const ask

            a <- readFile ""
            liftIO (a `shouldBe` "read me")

            local (++ " too") $ do
              b <- readFile ""
              liftIO (b `shouldBe` "read me too")

    it "has a correct implementation of MonadState" $
      example $ do
        filesRead <- flip execStateT (0 :: Int) $
          runMockT $ do
            expectAny $
              ReadFile_ anything
                |=> \_ -> modify (+ 1) >> return ""

            _ <- readFile "foo.txt"
            _ <- readFile "bar.txt"
            return ()

        filesRead `shouldBe` 2

    it "describes expectations when asked" $
      example . runMockT $ do
        expectAny $ ReadFile_ anything
        expectAny $
          WriteFile_ anything anything
            `SuchThat` \(WriteFile a b) -> b `isInfixOf` a

        expectations <- describeExpectations

        -- Format is deliberately unspecified.  We're forcing it here so that
        -- test coverage doesn't flag the formatting code as untested.
        liftIO $ evaluate (rnf expectations)

    it "verifies expectations early" $
      example $ do
        let test = runMockT $ do
              expect $ ReadFile "foo.txt" |-> "lorem ipsum"
              verifyExpectations
              _ <- readFile "foo.txt"
              return ()

        test `shouldThrow` anyException

    it "allows the user to override a default with byDefault" $
      example $
        runMockT $ do
          expectAny $ ReadFile_ anything

          r1 <- readFile "foo.txt"

          byDefault $ ReadFile "foo.txt" |-> "foo"
          r2 <- readFile "foo.txt"
          r3 <- readFile "bar.txt"

          liftIO (r1 `shouldBe` "")
          liftIO (r2 `shouldBe` "foo")
          liftIO (r3 `shouldBe` "")

    it "adopts lax behavior for allowUnexpected" $
      example $
        runMockT $ do
          allowUnexpected $ ReadFile "foo.txt" |-> "foo"
          r <- readFile "foo.txt"
          liftIO (r `shouldBe` "foo")

    it "prefers expect over allowUnexpected" $
      example $
        runMockT $ do
          expectAny $ ReadFile_ anything |-> "bar"
          allowUnexpected $ ReadFile "foo.txt" |-> "foo"
          r <- readFile "foo.txt"
          liftIO (r `shouldBe` "bar")

    it "uses defaults when allowUnexpected is not explicit" $
      example $ do
        runMockT $ do
          byDefault $ ReadFile "foo.txt" |-> "foo" -- added before allow
          allowUnexpected $ ReadFile_ anything
          byDefault $ ReadFile "bar.txt" |-> "bar" -- added after allow
          result <-
            (,,)
              <$> readFile "foo.txt"
              <*> readFile "bar.txt"
              <*> readFile "baz.txt"
          liftIO (result `shouldBe` ("foo", "bar", ""))

    it "performs side effects" $
      example $
        runMockT $ do
          ref <- liftIO $ newIORef False

          allowUnexpected $ WriteFile_ anything anything
          whenever $
            WriteFile_ anything anything
              |=> const (liftIO (writeIORef ref True))

          writeFile "foo.txt" "foo"

          liftIO (readIORef ref `shouldReturn` True)

    it "doesn't adopt lax behavior for byDefault" $
      example $ do
        let test = runMockT $ do
              byDefault $ ReadFile "foo.txt" |-> "foo"
              readFile "foo.txt"
        liftIO (test `shouldThrow` anyException)

    it "checks ambiguity when asked" $
      example $ do
        let setExpectations = do
              expect $ ReadFile_ anything
              expect $ ReadFile "foo.txt"
              setAmbiguityCheck Error

            failure = runMockT $ do
              setExpectations
              _ <- readFile "foo.txt"
              _ <- readFile "bar.txt"
              return ()

            success = runMockT $ do
              setExpectations
              _ <- readFile "bar.txt"
              _ <- readFile "foo.txt"
              return ()

        success
        failure `shouldThrow` errorWith ("Ambiguous action" `isInfixOf`)

    it "ignores unexpected actions when asked" $
      example . runMockT $ do
        setUnexpectedActionCheck Ignore
        writeFile "foo.txt" "unexpected"

    it "ignores uninteresting actions when asked" $
      example . runMockT $ do
        setUninterestingActionCheck Ignore
        writeFile "foo.txt" "unexpected"

    it "still catches unexpected actions when ignoring uninteresting actions" $
      example $ do
        let test = runMockT $ do
              setUninterestingActionCheck Ignore
              expect $ WriteFile "bar.txt" ""

              writeFile "foo.txt" "unexpected"

        test `shouldThrow` anyException

    it "ignores unmet expectations when asked" $
      example . runMockT $ do
        setUnmetExpectationCheck Ignore
        expect $ ReadFile_ anything

    describe "nestMockT" $ do
      it "checks nested context early" $ do
        example $ do
          let success = runMockT $ do
                expect $ WriteFile "final.txt" "final"
                nestMockT $ do
                  expect $ WriteFile "foo.txt" "foo"
                  writeFile "foo.txt" "foo"
                writeFile "final.txt" "final"

              failure = runMockT $ do
                nestMockT $ do
                  expect $ WriteFile "foo.txt" "foo"
                writeFile "foo.txt" "foo"

          success
          failure `shouldThrow` anyException

      it "updates the right context when nesting" $
        example $
          runMockT $ do
            expect $ ReadFile "foo.txt" |-> "foo #1" |-> "foo #2"
            result <- nestMockT $ do
              expect $ ReadFile "bar.txt" |-> "bar #1" |-> "bar #2"
              (,,,)
                <$> readFile "foo.txt"
                <*> readFile "bar.txt"
                <*> readFile "foo.txt"
                <*> readFile "bar.txt"
            liftIO (result `shouldBe` ("foo #1", "bar #1", "foo #2", "bar #2"))

      it "inherits defaults correctly" $
        example $
          runMockT $ do
            allowUnexpected $ WriteFile_ anything anything

            superCount <- liftIO $ newIORef (0 :: Int)
            subCount <- liftIO $ newIORef (0 :: Int)

            whenever $
              WriteFile_ anything anything
                |=> \_ -> liftIO $ modifyIORef superCount (+ 1)
            writeFile "foo.txt" "foo"

            nestMockT $ do
              whenever $
                WriteFile_ anything anything
                  |=> \_ -> liftIO $ modifyIORef subCount (+ 1)
              writeFile "foo.txt" "foo"

            writeFile "foo.txt" "foo"

            liftIO (readIORef superCount `shouldReturn` 3)
            liftIO (readIORef subCount `shouldReturn` 1)

      it "inherits side effects correctly" $
        example $
          runMockT $ do
            allowUnexpected $ ReadFile "foo.txt" |-> "foo"
            allowUnexpected $ ReadFile "bar.txt" |-> "bar"
            result <- nestMockT $ do
              allowUnexpected $ ReadFile "foo.txt" |-> "foo #2"
              (,) <$> readFile "foo.txt" <*> readFile "bar.txt"

            liftIO (result `shouldBe` ("foo #2", "bar"))

            result2 <- readFile "foo.txt"
            liftIO (result2 `shouldBe` "foo")

errorWith :: (String -> Bool) -> SomeException -> Bool
errorWith p e = p (show e)

(<&&>) :: Applicative f => f Bool -> f Bool -> f Bool
x <&&> y = (&&) <$> x <*> y