Reputation: 485
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
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.
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.
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
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