Testing functions in Haskell that do IO

You can make your code testable by using a type-class-constrained type variable instead of IO.

First, let’s get the imports out of the way.

{-# LANGUAGE FlexibleInstances #-}
import qualified Prelude
import Prelude hiding(readFile)
import Control.Monad.State

The code we want to test:

class Monad m => FSMonad m where
    readFile :: FilePath -> m String

-- | 4) Counts the number of characters in a file
numCharactersInFile :: FSMonad m => FilePath -> m Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)

Later, we can run it:

instance FSMonad IO where
    readFile = Prelude.readFile

And test it too:

data MockFS = SingleFile FilePath String

instance FSMonad (State MockFS) where 
               -- ^ Reader would be enough in this particular case though
    readFile pathRequested = do
        (SingleFile pathExisting contents) <- get
        if pathExisting == pathRequested
            then return contents
            else fail "file not found"


testNumCharactersInFile :: Bool
testNumCharactersInFile =
    evalState
        (numCharactersInFile "test.txt") 
        (SingleFile "test.txt" "hello world")
      == 11

This way your code under test needs very little modification.

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)