Take the 2-minute tour ×

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.

share|improve this question
What then will remain to test after "mocking" filesystem? length function? –  Alexander Poluektov Sep 10 '11 at 6:48
Haskell may place an emphasis on purity, but the IO monad is NOT pure. –  Dietrich Epp Sep 10 '11 at 7:16
If you did something more interesting than length on the contents of the file, you could easily test that function, which would be String -> a for some a. –  Dan Burton Sep 10 '11 at 7:27
@Dietrich Yes it is. Its unsafePerformIO that is impure ;) –  alternative Sep 10 '11 at 15:14
@Dietrich No. >>= and >> and return are pure. –  alternative Sep 11 '11 at 14:45

5 Answers 5

up vote 12 down vote accepted

As Alexander Poluektov already pointed out, the code you are trying to test can easily be separated into a pure and an impure part. Nevertheless I think it is good to know how to test such impure functions in haskell.
The usual approach to testing in haskell is to use quickcheck and that's what I also tend to use for impure code.

Here is an example of how you might achieve what you are trying to do which gives you kind of a mock behavior * :

import Test.QuickCheck
import Test.QuickCheck.Monadic(monadicIO,run,assert)
import System.Directory(removeFile,getTemporaryDirectory)
import System.IO
import Control.Exception(finally,bracket)

numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)

Now provide an alternative function (Testing against a model):

numAlternative ::  FilePath -> IO Integer
numAlternative p = bracket (openFile p ReadMode) hClose hFileSize

Provide an Arbitrary instance for the test environment:

data TestFile = TestFile String deriving (Eq,Ord,Show)
instance Arbitrary TestFile where
  arbitrary = do
    n <- choose (0,2000)
    testString <- vectorOf n $ elements ['a'..'z'] 
    return $ TestFile testString

Property testing against the model (using quickcheck for monadic code):

prop_charsInFile (TestFile string) = 
  length string > 0 ==> monadicIO $ do
    (res,alternative) <- run $ createTmpFile string $
      \p h -> do
          alternative <- numAlternative p
          testRes <- numCharactersInFile p
          return (testRes,alternative)
    assert $ res == fromInteger alternative

And a little helper function:

createTmpFile :: String -> (FilePath -> Handle -> IO a) -> IO a
createTmpFile content func = do
      tempdir <- catch getTemporaryDirectory (\_ -> return ".")
      (tempfile, temph) <- openTempFile tempdir ""
      hPutStr temph content
      hFlush temph
      hClose temph
      finally (func tempfile temph) 
              (removeFile tempfile)

This will let quickCheck create some random files for you and test your implementation against a model function.

$ quickCheck prop_charsInFile 
+++ OK, passed 100 tests.

Of course you could also test some other properties depending on your usecase.

* Note about the my usage of the term mock behavior:
The term mock in the object oriented sense is perhaps not the best here. But what is the intention behind a mock?
It let's you test code that needs access to a resource that usually is

  • either not available at testing time
  • or is not easily controllable and thus not easy to verify.

By shifting the responsibility of providing such a resource to quickcheck, it suddenly becomes feasible to provide an environment for the code under test that can be verified after a test run.
Martin Fowler describes this nicely in an article about mocks :
"Mocks are ... objects pre-programmed with expectations which form a specification of the calls they are expected to receive."
For the quickcheck setup I'd say that files generated as input are "pre-programmed" such that we know about their size (== expectation). And then they are verified against our specification (== property).

share|improve this answer
This doesn't act, speak, or talk like a mock. A mock should not be touching the file system. I do think the OP might need to introduce a typeclass to abstract the use of IO in order to use mock object, just as any other language does. I.e in C++ virtual functions are required, in Java, interfaces are required to use mocking frameworks. –  user239558 Sep 12 '11 at 10:25
@user239558: good remark. I added a clarification to my usage of the term mock. –  oliver Sep 12 '11 at 17:20

You can make your code testable by using a type-class-constrained type variable instead of IO. You can, for example, do this:

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

numCharactersInFile :: FSMonad m => FilePath -> m Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)

Later, you can run it in IO:

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

As you can see, this way your code under test needs the least amount of modification.

The full code can be found here: http://hpaste.org/51210

share|improve this answer
This is the only correct approach IMO. It is unfortunate that these isn't a set of standard mockable typeclasses for this, along with standard mockability. –  user239558 Sep 10 '11 at 22:01
Agreed. Best attempt at addressing a "real" mock. –  David Hinkes Jan 18 '12 at 22:19
This solution uses TypeSynonymInstances. Is this frowned upon? –  David Hinkes Jan 27 '12 at 7:14
@Dave, that extension is only used in the testing code for brewity and you can easily avoid that by making a newtype. –  Rotsor Jan 27 '12 at 13:21

The function consists from two parts: impure (reading part content as String) and pure (calculating the length of String).

The impure part cannot be "unit"-tested by definition. The pure part is just call to the library function (and of course you can test it if you want :) ).

So there is nothing to mock and nothing to unit-test in this example.

Put it another way. Consider you have an equal C++ or Java implementation (*): reading content and then calculating its length. What would you really want to mock and what would remain for testing afterwards?

(*) which is of course not the way you will do in C++ or Java, but that's offtopic.

share|improve this answer
Downvoter: care to explain? –  Alexander Poluektov Sep 11 '11 at 8:11
I have downvoted because this is not an answer. This answer can be summarized to "you can't", which is not really true and thus not useful. Also, I fail to see what definition of unit-testing disallows testing of impure functions (they do it all the time in Java :D). –  Rotsor Sep 11 '11 at 9:53
I agree and disagree with this post. The key insight is right -- test the pure part and impure parts separately. The "mock" of the results of a readFile is just a string -- precisely what you pass to length to begin with! My disagreement is just that one can of course unit-test readFile -- its just that when doing so, one of course should not mock it. –  sclv Sep 12 '11 at 19:22
@sclv All in all, it depends on what definition of the "unit-testing" you use. I use one that prohibits touching functions that do IO things. –  Alexander Poluektov Sep 12 '11 at 19:29
FWIW, the question as it stands does not contain the word 'unit'. –  steamer25 Sep 12 '11 at 21:55

For that you will need to modify the function such that it becomes:

numCharactersInFile :: (FilePath -> IO String) -> FilePath -> IO Int
numCharactersInFile reader fileName = do
                         contents <- reader fileName
                         return (length contents)

Now you can pass any mock function that takes a file path and return IO string such as:

fakeFile :: FilePath -> IO String
fakeFile fileName = return "Fake content"

and pass this function to numCharactersInFile.

share|improve this answer
+1: Answers the question, but looks very un-Haskell-ish IMO –  Alexander Poluektov Sep 10 '11 at 8:10
Replace IO with Monad m => m and it becomes Haskell-ish enough. –  Rotsor Sep 10 '11 at 21:04

Based on my layman's understanding of Haskell, I've come to the following conclusions:

  1. If a function makes use of the IO monad, mock testing is going to be impossible. Avoid hard-coding the IO monad in your function.

  2. Make a helper version of your function that takes in other functions that may do IO. The result will look like this:

numCharactersInFile' :: Monad m => (FilePath -> m String) -> FilePath -> m Int
numCharactersInFile' f filePath = do
    contents <- f filePath
    return (length contents)

numCharactersInFile' is now testable with mocks!

mockFileSystem :: FilePath -> Identity String
mockFileSystem "fileName" = return "mock file contents"

Now you can verify that numCharactersInFile' returns the the expected results w/o IO:

18 == (runIdentity .  numCharactersInFile' mockFileSystem $ "fileName")

Finally, export a version of your original function signature for use with IO

numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile

So, at the end of the day, numCharactersInFile' is testable with mocks. numCharactersInFile is just a variation of numCharactersInFile'.

share|improve this answer
FYI, this is a combination of Ankur's solution combined incorporating Rotsor's comment. –  David Hinkes Feb 5 '13 at 1:04

Your Answer


