Working through Real World Haskell right now. Here's a solution to a very early exercise in the book:
-- | 4) Counts the number of characters in a file numCharactersInFile :: FilePath -> IO Int numCharactersInFile fileName = do contents <- readFile fileName return (length contents)
My question is: How would you test this function? Is there a way to make a "mock" input instead of actually needing to interact with the file system to test it out? Haskell places such an emphasis on pure functions that I have to imagine that this is easy to do.
IO actions are used to affect the world outside of the program. Actions take no arguments but have a result value. Actions are inert until run. Only one IO action in a Haskell program is run ( main ). Do-blocks combine multiple actions together into a single action.
The I/O monad contains primitives which build composite actions, a process similar to joining statements in sequential order using `;' in other languages. Thus the monad serves as the glue which binds together the actions in a program.
The IO type constructor provides a way to represent actions as Haskell values, so that we can manipulate them with pure functions. In the Prologue chapter, we anticipated some of the key features of this solution. Now that we also know that IO is a monad, we can wrap up the discussion we started there.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With