Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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"
like image 690
Erik Avatar asked Oct 30 '25 12:10

Erik


1 Answers

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.

like image 152
Mark Seemann Avatar answered Nov 01 '25 14:11

Mark Seemann