2014-12-02 64 views
4

我试图测试一个小函数(或更确切地说,IO操作),它接受一个命令行参数并将其输出到屏幕。我原来(不可测)函数是:嘲笑IO操作:getArgs和putStrLn

-- In Library.hs 
module Library where 

import System.Environment (getArgs) 

run :: IO() 
run = do 
    args <- getArgs 
    putStrLn $ head args 

看着this answer about mocking后,我想出了一个办法是用一种类约束类型嘲笑getArgsputStrLn。所以上面的函数变为:

-- In Library.hs 
module Library where 

class Monad m => SystemMonad m where 
    getArgs :: m [String] 
    putStrLn :: String -> m() 

instance SystemMonad IO where 
    getArgs = System.Environment.getArgs 
    putStrLn = Prelude.putStrLn 

run :: SystemMonad m => m() 
run = do 
    args <- Library.getArgs 
    Library.putStrLn $ head args 

Library.Prelude.System.Environment.是避免Ambigious Occurence编译器的投诉。我的测试文件如下所示。

-- In LibrarySpec.hs 
{-# LANGUAGE TypeSynonymInstances #-} 
{-# LANGUAGE FlexibleInstances #-} 

import Library 
import Test.Hspec 
import Control.Monad.State 

data MockArgsAndResult = MockArgsAndResult [String] String 
    deriving(Eq, Show) 

instance SystemMonad (State MockArgsAndResult) where 
    getArgs = do 
     MockArgsAndResult args _ <- get 
     return args 
    putStrLn string = do 
     MockArgsAndResult args _ <- get 
     put $ MockArgsAndResult args string 
     return() 

main :: IO() 
main = hspec $ do 
    describe "run" $ do 
    it "passes the first command line argument to putStrLn" $ do 
     (execState run (MockArgsAndResult ["first", "second"] "")) `shouldBe` (MockArgsAndResult ["first", "second"] "first") 

我正在使用State monad,它有效地包含2个字段。

  1. 一种命令行参数,其中模拟getArgs
  2. 读取一个字符串,模拟putStrLn把什么传递给它的列表。

上面的代码工程,似乎测试我想要它测试。但是,我想知道是否有一些更好/更清洁/更习惯性的测试方式。一方面,我使用的是相同的状态既把东西到测试(我的假命令行参数),然后拿东西出来的(东西传递给putStrLn

是否有更好的办法做我在干什么?我更熟悉Javascript环境中的嘲讽,并且我对Haskell的知识是非常基础的(我通过一些反复试验来达到上述解决方案,而不是真正理解)

+1

我认为你的代码很好/干净/习惯用法。真正的问题是这能解决你的问题吗?您的“模型”是否足以准确地表示您正在建模的实际“文件系统”?如果是这样,那么可能没有必要过时这一点。但是,如果你真的想模拟'Prelude.putStrLn','putStrLn'应该把它的字符串参数附加到状态中的旧字符串,而不是忽略旧字符串。 – user2407038 2014-12-03 00:11:27

+0

@ user2407038追加到旧字符串听起来像个好主意。 – 2014-12-03 06:25:35

回答

1

更好的方法是避免需要通过分离出计算的心脏为一个纯函数提供的getArgsputStrLn模拟版本。

有限公司nsider这个例子:

main = do 
    args <- getArgs 
    let n = length $ filter (\w -> length w < 5) args 
    putStrLn $ "Number of small words: " ++ show n 

可以说,计算的心脏计数的小词的数量是[String] -> Int类型的纯函数。这表明,我们要重构的程序是这样的:

main = do 
    args <- getArgs 
    let n = countSmallWords args 
    putStrLn $ "Number of small words: " ++ show n 

countSmallWords :: [String] -> Int 
countSmallWords ws = ... 

现在我们只是测试countSmallWords,这是容易的,因为它是纯函数。

+0

是的,分离出来的纯函数看起来不错,但总会有一些IO动作(返回IO动作的/函数)留下来,看起来需要测试,至少它们传递的是纯函数的结果。 – 2014-12-03 06:35:49

+0

拿出一个例子,我们可以探索组织它的方法,使测试更容易。 – ErikR 2014-12-03 07:17:25

+0

呃...我原来的问题中的一个,以及您的答案中的一个似乎是很好的例子!也许我不明白你要求什么? – 2014-12-03 07:23:32