6

我正在尝试测试一个小函数(或者更确切地说,IO Action),它接受一个命令行参数并将其输出到屏幕上。我原来的(不可测试的)功能是:

-- In Library.hs
module Library where

import System.Environment (getArgs)

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

在查看了这个关于 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有效包含 2 个字段的 monad。

  1. getArgs模拟从中读取的命令行参数列表
  2. putStrLn模拟放置传递给它的内容的字符串。

上面的代码有效,似乎测试了我想要测试的内容。但是,我想知道是否有更好/更清洁/更惯用的测试方法。一方面,我使用相同的状态将东西放入测试(我的假命令行参数),然后从中取出东西(传递给putStrLn.

有没有更好的方法来做我正在做的事情?我更熟悉在 Javascript 环境中进行模拟,并且我对 Haskell 的了解非常基础(我通过大量试验和错误得出上述解决方案,而不是实际理解)

4

1 回答 1

2

更好的方法是避免提供模拟版本,getArgs并将putStrLn计算的核心分离为纯函数。

考虑这个例子:

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

可以说计算的核心是计算小单词的数量,这是 type 的纯函数[String] -> Int。这表明我们应该像这样重构程序:

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

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

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

于 2014-12-03T00:41:59.447 回答