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