Øyvind Roth
Øyvind Roth

Reputation: 255

How do I capture stdout and simulate stdin in a Haskell Tasty test?

My impure functions are defined as follows:

module HangmanImpure (HangmanImpure.getLine, HangmanImpure.putStr, HangmanImpure.putStrLn, HangmanImpure.strlen) where

import qualified System.IO as Sys

getLine :: IO String
getLine = do x <- Sys.getChar
             if x == '\n' then
                 return []
             else
                do xs <- HangmanImpure.getLine
                   return (x:xs)

putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do Sys.putChar x
                   HangmanImpure.putStr xs

I want to test these functions using Tasty, like e.g. so:

module TestHangmanImpure (scProps) where

import qualified Test.Tasty as T
import qualified Test.Tasty.SmallCheck as SC

import qualified HangmanImpure as HI (getLine, putStr, putStrLn, strlen)

scProps :: T.TestTree
scProps = T.testGroup "checked by SmallCheck"
    [
        SC.testProperty "getLine should return what is written to stdin" $
            "Placeholder to get the test to run" == "Placeholder to get the test to run" -- HI.getLine == IO "Hello" -- PLEASE, HELP ME WITH SOME CONTENT HERE

        , SC.testProperty "putStr should write to stdout what it gets in as an argument" $
            "Placeholder to get the test to run" == "Placeholder to get the test to run" -- HI.putStr "Nice day!" -- PLEASE, HELP ME WITH SOME CONTENT HERE
    ]

I would be very thankful if anybody could fill in the body in the two simple tests I have indicated.

EDIT: I think [In haskell, how can I interact with stdin of an IO ()?][1] could lead me to a solution. I can get that solution to work on my Win10 with HSpec. So this question is almost a duplicate. But I cannot get it to work using Tasty.HUnit. My tentative code runs as follows:

module TestUtilImpure (scProps, qcProps, uTests, properties) where
    
import qualified Test.Tasty as T
import qualified Test.Tasty.HUnit as HU
    
import StdInStdOutUtil as IOU (captureStdout, provideStdin)
import UtilImpure as UI (getLine)
    
properties :: T.TestTree
properties = T.testGroup "TestUtilImpure tests" [
          uTests
        ]
    
uTests :: T.TestTree
uTests = T.testGroup "Checked by HUnit"
        [
            HU.testCase "UI.getLine reading from stdin should return what was provided" $ do
                let line = "Hello, echo!\n"
                let capturedIOString = IOU.captureStdout (provideStdin line echo)
                capturedString <- capturedIOString
                HU.assertBool "The String returned from UI.getLine differs from the one provided for StdIn" $ capturedString == line
        ]

echo :: IO ()
echo = UI.getLine >>= Prelude.putStrLn

(I have put the solution from [1]: In haskell, how can I interact with stdin of an `IO ()`? into its own file referred by

 import StdInStdOutUtil as IOU (captureStdout, provideStdin)

My test fails:

The String returned from UI.getLine differs from the one provided for StdIn

I get the same error message whether I test Prelude.getLine or my own UI.getLine. So I guess that there's some syntax I have not understood using Tasty that fails.

Upvotes: 1

Views: 148

Answers (1)

I am using Test.Tasty.HIOUnit to test IO functions.

At the moment, HIOUnit is only available as source code at https://github.com/JoergBrueggmann/tasty-hiounit.

A test spec with HIOUnit looks like this:

import qualified Test.Tasty as T

import qualified Test.Tasty.HIOUnit as HIOU


testGroup :: T.TestTree
testGroup =
    T.testGroup
        "module Test.Tasty.HIOUnit"
        [
            tgFib
        ]

tgFib :: T.TestTree
tgFib = 
    T.testGroup
        "Fib.ioFib"
        [
            -- edge cases using values arround zero
            HIOU.testCase "Fib.ioFib -2" (HIOU.IOTestParameter ( Just "-2" ) fdefFib ( Just "-1" ) ( Just "" ) ( Just 0 )), 
            HIOU.testCase "Fib.ioFib -1" (HIOU.IOTestParameter ( Just "-1" ) fdefFib ( Just "1" ) ( Just "" ) ( Just 0 )), 
            HIOU.testCase "Fib.ioFib 0" (HIOU.IOTestParameter ( Just "0" ) fdefFib ( Just "0" ) ( Just "" ) ( Just 0 )), 
            HIOU.testCase "Fib.ioFib 1" (HIOU.IOTestParameter ( Just "1" ) fdefFib ( Just "1" ) ( Just "" ) ( Just 0 )), 
            HIOU.testCase "Fib.ioFib 2" (HIOU.IOTestParameter ( Just "2" ) fdefFib ( Just "1" ) ( Just "" ) ( Just 0 )), 
            HIOU.testCase "Fib.ioFib 3" (HIOU.IOTestParameter ( Just "3" ) fdefFib ( Just "2" ) ( Just "" ) ( Just 0 ))
        ]

fdefFib :: HIOU.IOFunctionDefinition
fdefFib = 
    HIOU.IOFunctionDefinition
        "Fib"
        (Just "ioFib")
        [HIOU.sMultiLine|
import qualified System.IO as Sys


ioFib :: IO ()
ioFib = 
    do
        Sys.hSetBuffering Sys.stdout Sys.NoBuffering
        sNumber <- getLine
        let
            niNumber = (read sNumber) :: Integer
        putStr (show (fib niNumber))

fib :: (Integral n) => n -> n
fib n 
    | n >= 0 = fib' n (0,1)
    | even n = 0 - fib (-n)
    | otherwise = fib (-n)
  where
    fib' n' (a, b)
        | n'==0      = a
        | otherwise = fib' (n'-1) (b, a+b)
|]

Upvotes: 1

Related Questions