Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing functions in Haskell that do IO

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.

like image 257
Derek Thurn Avatar asked Sep 10 '11 06:09

Derek Thurn


People also ask

What is IO action in Haskell?

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.

Why is IO a Monad in Haskell?

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.

Is IO a Monad Haskell?

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.


1 Answers

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.

like image 172
Rotsor Avatar answered Oct 20 '22 15:10

Rotsor