Writing a unit test in Haskell where an expression should fail when undefined
is encountered is a bit tricky. I tried the following with HSpec:
module Main where
import Test.Hspec
import Control.Exception (evaluate)
main :: IO ()
main = hspec $ do
describe "Test" $ do
it "test case" $ do
evaluate (take 1 $ map (+1) [undefined, 2, 3]) `shouldThrow` anyException
to no avail. It reports me did not get expected exception: SomeException
If I evaluate the same expression in REPL, I'd get:
[*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:2:20 in interactive:Ghci1
The problem is that evaluate
doesn't force your expression to NH or even WHNF1. Try x <- evaluate (take 1 $ map (+1) [undefined, 2, 3])
in GHCi - it doesn't give you any error! The only reason it does when you paste in evaluate (take 1 $ map (+1) [undefined, 2, 3])
is that GHCi also tries to print the result of what it got and, to do that, it ends up trying to evaluate the expression.
If you want to see how much of a thunk has been evaluated, you can always use :sprint
in GHCi:
ghci> x <- evaluate (take 1 $ map (+1) [undefined, 2, 3])
ghci> :sprint x
x = [_]
As you can see, evaluate
hasn't forced the expression far enough to realize x
contains an undefined
. A quick fix is to evaluate the thing you are examining to normal form using force
.
import Test.Hspec
import Control.Exception (evaluate)
import Control.DeepSeq (force)
main :: IO ()
main = hspec $ do
describe "Test" $ do
it "test case" $ do
evaluate (force (take 1 $ map (+1) [undefined, 2, 3] :: [Int]))
`shouldThrow` anyException
force
lets you trigger the evaluation of thunks until the argument is full evaluated. Note that it has an NFData
(stands for "normal form data") constraint on it, so you may find yourself deriving Generic
and NFData
for your data structures.
1 Thanks for @AlexisKing for pointing out that evaluate
does push its argument into WNHF, which is why head $ map (+1) [undefined, 2, 3]
does trigger the error. In the case of take
, it isn't enough though.