4

I have found other questions on similar lines but nothing that answers my question in this particular scenario. Furthermore, there seem to be few resources which succinctly cover the subject of unit testing IO actions in Haskell.

Let's say I have this typeclass for my database communication:

data Something = Something String deriving Show

class MonadIO m => MonadDB m where
  getSomething :: String -> m Something
  getSomething s = do
    ... -- assume a DB call is made and an otherwise valid function

instance MonadDB IO 

and this function which uses it:

getIt :: MonadDB m => m (Int, Something)
getIt = do
  s@(Something str) <- getSomething "hi"
  return (length str, s) -- excuse the contrived example

I wish to test this getIt function with hspec but without it talking to the database, which presumably means replacing which MonadDB it uses, but how do I achieve that?

Alex
  • 8,093
  • 6
  • 49
  • 79

1 Answers1

6

Would this work for you?

#!/usr/bin/env stack
-- stack exec --package transformers --package hspec -- ghci
import Control.Monad.IO.Class
import Control.Monad.Trans.Identity

import Data.Char
import Test.Hspec

data Something = Something String deriving (Eq, Show)

class MonadIO m => MonadDB m where
  getSomething :: String -> m Something
  getSomething s = return $ Something (map toUpper s)

instance MonadDB IO

instance MonadIO m => MonadDB (IdentityT m)

getIt :: MonadDB m => m (Int, Something)
getIt = do
  s@(Something str) <- getSomething "hi"
  return (length str, s)

main :: IO ()
main = hspec $ do
  describe "Some tests" $ do
    it "test getIt" $ do
      runIdentityT getIt `shouldReturn` (2, Something "HI")

    it "test getIt should fail" $ do
      runIdentityT getIt `shouldReturn` (1, Something "HI")

You might also be able to use ReaderT or StateT to "supply" data or a transformation for getSomething to use upon test querying.

Edit: Example use from within hspec.

David McHealy
  • 2,471
  • 18
  • 34
  • Thanks. How would your test run inside a HSpec spec? – Alex Jan 18 '17 at 21:02
  • Updated my answer. – David McHealy Jan 19 '17 at 21:48
  • Does the `MonadDB` being a different package have any bearing on the ability for the compiler to select the `IdentityT m` instance? – Alex Jan 19 '17 at 22:23
  • I don't think so. Are you having trouble? – David McHealy Jan 19 '17 at 23:06
  • Yes, the code's in another module inside a `Pipe`, but I didn't think that'd matter. Should I update my question? – Alex Jan 20 '17 at 07:44
  • Or rather, the calls to said typeclass functions are via `liftIO`...Could that have an impact? – Alex Jan 20 '17 at 07:47
  • I'm pretty sure you can run `getIt` within pipes with little change. But without seeing the code I'm not sure what your problem might be. Might be better to make a new question rather than editing this one. – David McHealy Jan 20 '17 at 12:35
  • Yep, will do. Thanks :) – Alex Jan 20 '17 at 12:42
  • I don't see how this helps. Where exactly does the DB call get mocked? Using `shouldReturn :: (HasCallStack, Show a, Eq a) => IO a -> a -> Expectation` implies that `runIdentityT getIt :: IO (Int, Something)` so it would call `IO` implementation of `getSomething` which calls the DB. How is `IdentityT` helping? – Random dude Jun 13 '20 at 01:25