4

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"
6
  • 1
    If mocking is part of the grade, your teacher probably should not have let you use Haskell in the first place. The idiomatic approach in Haskell is to use types that don't require you to mock anything in the first place. Commented Nov 7, 2019 at 14:22
  • 1
    I know that It would be much easier for me to get a higher grade if I would use Java or Python. But where is the fun in that (=. I dont really care about the grade, as long as i pass the course. I choose Haskell to learn more about Haskell. Maybe I should try to convince him that mocking isn't really used in Haskell? Commented Nov 7, 2019 at 14:26
  • You could try anyway, even if it's not that idiomatic in Haskell. You could rename all the IO actions to use a module like IO.putStrLn "...", and then import System.IO as IO or import MyMockIO as IO to 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. Commented Nov 7, 2019 at 14:54
  • For very basic mocking, maybe you could even use the pure-io package, if you don't need very fancy IO. hackage.haskell.org/package/pure-io-0.2.1/docs/PureIO.html Commented Nov 7, 2019 at 14:56
  • Lets say I create in MyMockIO IO.getLine = "1+2" Im not sure how this helps me in my testing. I would need getLine to first return "1+2" and then "quit" otherwise we will be stuck in main forever. @chi, but I guess I just dont understand what you mean. Commented Nov 7, 2019 at 17:22

2 Answers 2

4

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.

Sign up to request clarification or add additional context in comments.

6 Comments

Thank you for a very detailed answer! I will hopefully get it to work.
Im not sure what to install so I can get these to work: import Test.Framework (defaultMain) import Test.Framework.Providers.HUnit import Q58750508 I tried hackage.haskell.org/package/HTF but I get Module ‘Test.Framework’ does not export ‘defaultMain’ Failed to load interface for ‘Test.Framework.Providers.HUnit’
I got it to work! I just needed to install this Test instead batterseapower.github.io/test-framework for: import Test.Framework (defaultMain) And hackage.haskell.org/package/test-framework-hunit for: import Test.Framework.Providers.HUnit But I still dont know why I need import Q58750508 or how to obtain it?
I do think this should be a completely new stackoverflow-post. But what is the best way to update mainImp when main is changed?
@ErikTöger The test project in the above PoC has these dependencies, in addition to the SUT: transformers, HUnit, test-framework, test-framework-hunit. I don't understand the question about changing main - you don't change main, you change mainImp...
|
4

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.

4 Comments

here is what I have done so far: [repl.it/@erikTeacher/Checking-Flow] I dont think this is what you intended though. But my code maybe shows how little Haskell I do know, so that It is easier for you (or others) to point me to relevant learning material.
@ErikTöger There's no need to use unsafePerformIO or to make the mutable references and functions top-level. Just create them in the IO do-block that holds your test and your assertions, it's easier that way. The references don't need to survive the test itself, after all. pseudoGetLine can also be defined within the block and passed as an argument inside the block. This link explains how lets let you create local definitions in a do-block en.wikibooks.org/wiki/Haskell/…
thanks! I have now updated the code in the repl. My next step will be to write code that modifies my original meny to create the mockmeny code. So that when I change my orginal meny code, the mockmeny code also change.
@ErikTöger About mockmeny in repl.it. The point of mocking is keeping the exact same business logic that you use in reality, and only substitute the dependencies. But you are tying your logic irrevocably to how you define pseudoPutStrLn. You cease to be able to use the real putStrLn! Make your business logic a separate top level function that takes the print and read actions as parameters. Then you can either pass the real putStrLn and readLine (for actual use) or the mocked ones that you create inside your test.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.