Erik
Erik

Reputation: 485

How to test small program with user input in Haskell

Im taking a software testing class. And we were allowed to choose what language to write our code and tests in. And I choose Haskell. I know, maybe not the best way to learn testing (= but...

The unit-testing and coding has been working great!

But I am also required to use mocking to get a higher grade.

My problem is that I don't know very much Haskell (like Monads and stuff).

I have written and tested Calculator. And now I want to test my main. My instructor used Mockito for Java to check that the program had the correct flow.

Is it possible to test that my if-statements are correct? I have tried reading up on testing IO actions through Monads, but I don't quite understand it. Maybe I should just learn more about Monads before trying to solve this?

Any help or reading suggestions are very much appreciated!

import Calculator

main :: IO ()
main = do
        putStrLn("What should I calculate? ex 3*(2+2)  | quit to exit" )
        line <- getLine
        if line /= "quit"
        then do if correctInput line
                then do putStrLn( show $ calculate line) 
                        main
                else do putStrLn "Wrong input" 
                        main
        else putStrLn "goodbye"

Upvotes: 4

Views: 1032

Answers (2)

Mark Seemann
Mark Seemann

Reputation: 233170

Even in an object-oriented environment like Java or C# (my other specialty), one wouldn't be able to use Test Doubles ('mocks') on the Main method, since you can't do dependency injection on the entry point; its signature is fixed.

What you'd normally do would be to define a MainImp that takes dependencies, and then use Test Doubles to test that, leaving the actual Main method as a Humble Executable.

Production code

You can do the same in Haskell. A simple approach is to do what danidiaz suggests, and simply pass impure actions as arguments to mainImp:

mainImp :: Monad m => m String -> (String -> m ()) -> m ()
mainImp getInput displayOutput = do
  displayOutput "What should I calcuclate? ex 3*(2+2)  | quit to exit" 
  line <- getInput
  if line /= "quit"
  then do if correctInput line
          then do displayOutput $ show $ calculate line
                  mainImp getInput displayOutput
          else do displayOutput "Wrong input"
                  mainImp getInput displayOutput
  else displayOutput "goodbye"

Notice that the type declaration explicitly allows any Monad m. This includes IO, which means that you can compose your actual main action like this:

main :: IO ()
main = mainImp getLine putStrLn

In tests, however, you can use another monad. Usually, State is well-suited to this task.

Tests

You can begin a test module with appropriate imports:

module Main where

import Control.Monad.Trans.State
import Test.HUnit.Base (Test(..), (~:), (~=?), (@?))
import Test.Framework (defaultMain)
import Test.Framework.Providers.HUnit
import Q58750508

main :: IO ()
main = defaultMain $ hUnitTestToTests $ TestList tests

This uses HUnit, and as you'll see in a while, I inline the tests in a list literal.

Before we get to the tests, however, I think it makes sense to define a test-specific type that can hold the state of the console:

data Console = Console { outputs :: [String], inputs :: [String] } deriving (Eq, Show)

You also need some functions that correspond to getInput and displayOutput, but that run in the State Console monad instead of in IO. This is a technique that I've described before.

getInput :: State Console String
getInput = do
  console <- get
  let input = head $ inputs console
  put $ console { inputs = tail $ inputs console }
  return input

Notice that this function is unsafe because it uses head and tail. I'll leave it as an exercise to make it safe.

It uses get to retrieve the current state of the console, pulls the head of the 'queue' of inputs, and updates the state before returning the input.

Likewise, you can implement displayOutput in the State Console monad:

displayOutput :: String -> State Console ()
displayOutput s = do
  console <- get
  put $ console { outputs = s : outputs console }

This just updates the state with the supplied String.

You're also going to need a way to run tests in the State Console monad:

runStateTest :: State Console a -> a
runStateTest = flip evalState $ Console [] []

This always kicks off any test with empty inputs and empty outputs, so it's your responsibility as a test writer to make sure that the inputs always ends with "quit". You could also write a helper function to do that, or change runStateTest to always include this value.

A simple test, then, is:

tests :: [Test]
tests = [
  "Quit right away" ~: runStateTest $ do
    modify $ \console -> console { inputs = ["quit"] }

    mainImp getInput displayOutput

    Console actual _ <- get
    return $ elem "goodbye" actual @? "\"goodbye\" wasn't found in " ++ show actual
-- other tests go here...
]

This test just verifies that if you immediately "quit", the "goodbye" message is present.

A slightly more involved test could be:

  ,
  "Run single calcuation" ~: runStateTest $ do
    modify $ \console -> console { inputs = ["3*(2+2)", "quit"] }

    mainImp getInput displayOutput

    Console actual _ <- get
    let expected =
          [ "What should I calcuclate? ex 3*(2+2)  | quit to exit",
            "12",
            "What should I calcuclate? ex 3*(2+2)  | quit to exit",
            "goodbye"]
    return $ expected ~=? reverse actual

You can insert it before the closing ] in the above tests list, where the comment says -- other tests go here....

I have more articles on unit testing with Haskell than the articles I've linked to, so be sure to follow the links there, as well as investigate what other articles inhabit the intersection between the Haskell tag and the Unit Testing tag.

Upvotes: 4

danidiaz
danidiaz

Reputation: 27766

You can "mock" (or "fake") your dependencies without having to go beyond IO.

Your program logic interacts with the external world by way of putStrLn and getLine. But what does it know about them? Nothing, really, other than their types String -> IO () and IO String.

So we can abstract them away, turning your program logic into a function myProgramLogic :: (String -> IO ()) -> IO String -> IO () that receives the effectful actions as arguments. Passing dependencies as function arguments is a low-calorie version of dependency injection.

Now the problem becomes: how to mock putStrLn and getLine. Obviously the mocks can't be something interactive, as we want to do automated testing. But neither can they be boring actions like \_ -> return () and return "foo" which always do the same thing. They must have state, they must keep a record of their interactions with the program logic.

There is a module called Data.IORef in the standard library that lets you create and manipulate mutable references living in IO. What other languages would call "just a boring, run-of-the-mill variable".

This bit of code creates a mutable reference containing a list of strings, and also defines a pseudoGetLine function which extracts one of the strings each time it's executed:

main :: IO ()
main = do
    inputsRef <- newIORef ["foo","bar","baz"]
    let pseudoGetLine :: IO String
        pseudoGetLine = do
            atomicModifyIORef inputsRef (\inputs ->
                case inputs of
                    i : is -> (is,i) -- the i becomes the return value of pseudoGetLine 
                    [] -> error "fake inputs exhausted")
    sample <- pseudoGetLine
    print sample

You can see where this is going: after faking both dependencies and passing them to your logic, you can inspect the states of the mutable references (using a function like readIORef) in order to check that they are the expected ones.

Upvotes: 4

Related Questions