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.
IO.putStrLn "...", and thenimport System.IO as IOorimport MyMockIO as IOto switch to a custom mock-IO module you can write. I don't know if that would count as proper "mocking" as your instructor expects. Even if that does not work perfectly well, learning that it's not a very good idea could still be learning, and you could get credit for that, in theory. Asking your instructor in advance would be a good idea.pure-iopackage, if you don't need very fancy IO. hackage.haskell.org/package/pure-io-0.2.1/docs/PureIO.html