patternMinor
Hacky Haskell monadic testing
Viewed 0 times
testinghaskellmonadichacky
Problem
Edit for posterity: The library you're looking for is Hspec.
I recently started writing Haskell tests with HUnit.
My general setup looks something like this:
The semantics of this are fine with me: I'm trying to do expected–actual testing, not property testing like Quickcheck does.
However, there are two things that I don't like about the syntax:
My goal was to be able to write tests like this:
I managed to accomplish just that!
But to do so I had to resort to what I consider to be a pretty ugly monad.
Here's
```
module Describe(group, describe, toTests, (~:), (...)) where
import qualified Test.HUnit as H
data LeftList l r = LeftList [l] ()
deriving (Show)
instance Monad (LeftList l) where
(>>=) = error "LeftList does not support binding; use (>>) instead"
(LeftList xs a) >> (LeftList ys b) = LeftList (xs ++ ys) b
return x = LeftList [] ()
group :: String -> LeftList H.Test () -> LeftList H.Test ()
group s (LeftList xs ()) = LeftList [s H.~: xs] ()
(...) = group
infixr 9 ...
describe :: String -> H.Test -> LeftList H.Test ()
describe s x = LeftList [s H.~
I recently started writing Haskell tests with HUnit.
My general setup looks something like this:
import Test.HUnit
import Widget (foo, bar)
tests = TestList [ "foo" ~: testFoo
, "bar" ~: testBar
]
testFoo :: Test
testFoo = TestList
[ "with even numbers" ~:
4 ~=? foo 4
, "with odd numbers" ~:
0 ~=? foo 5
]
testBar :: Test
testBar = TestList [ {- omitted -} ]The semantics of this are fine with me: I'm trying to do expected–actual testing, not property testing like Quickcheck does.
However, there are two things that I don't like about the syntax:
- the list syntax feels really clunky, and
- I have to write
"foo"once andtestFootwo times (three counting the type annotations); while I could inline this, that would make the lists even clunkier.
My goal was to be able to write tests like this:
import Test.HUnit (Test, (~=?))
import Describe (toTests, (...), (~:))
import Widget (foo, bar)
tests :: Test
tests = toTests $ do
"foo" ... do
"with even numbers" ~:
4 ~=? foo 4
"with odd numbers" ~:
0 ~=? foo 5
"bar" ... do
"with true" ~:
10 ~=? bar True
"with false" ~:
-10 ~=? bar FalseI managed to accomplish just that!
But to do so I had to resort to what I consider to be a pretty ugly monad.
Here's
Describe.hs:```
module Describe(group, describe, toTests, (~:), (...)) where
import qualified Test.HUnit as H
data LeftList l r = LeftList [l] ()
deriving (Show)
instance Monad (LeftList l) where
(>>=) = error "LeftList does not support binding; use (>>) instead"
(LeftList xs a) >> (LeftList ys b) = LeftList (xs ++ ys) b
return x = LeftList [] ()
group :: String -> LeftList H.Test () -> LeftList H.Test ()
group s (LeftList xs ()) = LeftList [s H.~: xs] ()
(...) = group
infixr 9 ...
describe :: String -> H.Test -> LeftList H.Test ()
describe s x = LeftList [s H.~
Solution
Firstly, I would advise you to look at how blaze-html implements their monads for HTML templating. They are doing something very similar to what you want to do.
You can simplify your definitions a bit by removing the second field from the
Then every place where you use the constructor
You should also make
This makes these functions valid for any return type
Of course, you don't care what the return type is anyway. But GHC cares - and allowing a general return type might help with type checking.
It actually is possible to define bind for the
An error will occur only if the parameter to
For instance, this will not throw an error:
because the value
Finally, with GHC 7.10 you will also have to define Functor and Applicative instances for your monad:
You can simplify your definitions a bit by removing the second field from the
LeftList constructor:data LeftList a r = LeftList [a]Then every place where you use the constructor
LeftList you can omit the (now) extraneous (), e.g. the >> definition simplifies to:(LeftList xs) >> (LeftList ys) = LeftList (xs ++ ys)You should also make
group, describe, and toTests more general by using a type variable instead of ():group :: String -> LeftList H.Test r -> LeftList H.Test rThis makes these functions valid for any return type
r - not just ().Of course, you don't care what the return type is anyway. But GHC cares - and allowing a general return type might help with type checking.
It actually is possible to define bind for the
LeftList monad -- just pass in undefined or error "...":(>>=) (LeftList xs) f = let LeftList ys = f (error "LeftList does not support binding")
in LeftList (xs++ys)An error will occur only if the parameter to
f is actually evaluated.For instance, this will not throw an error:
"foo" ... do
x <- "with even numbers" ~:
4 ~=? foo 4
"with odd numbers" ~:
0 ~=? foo 5because the value
x is never evaluated.Finally, with GHC 7.10 you will also have to define Functor and Applicative instances for your monad:
instance Functor (LeftList a) where
fmap f left = left
instance Applicative (LeftList a) where
pure _ = LeftList [] -- should be same as return
() = undefinedCode Snippets
data LeftList a r = LeftList [a](LeftList xs) >> (LeftList ys) = LeftList (xs ++ ys)group :: String -> LeftList H.Test r -> LeftList H.Test r(>>=) (LeftList xs) f = let LeftList ys = f (error "LeftList does not support binding")
in LeftList (xs++ys)"foo" ... do
x <- "with even numbers" ~:
4 ~=? foo 4
"with odd numbers" ~:
0 ~=? foo 5Context
StackExchange Code Review Q#105842, answer score: 3
Revisions (0)
No revisions yet.