在Haskell中测试执行IO的函数

Der*_*urn 34 testing haskell

现在正在通过真实世界Haskell工作.这是本书早期练习的解决方案:

-- | 4) Counts the number of characters in a file
numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)
Run Code Online (Sandbox Code Playgroud)

我的问题是:你如何测试这个功能?有没有办法制作"模拟"输入而不是实际需要与文件系统进行交互来测试它?Haskell强调纯函数,我必须想象这很容易做到.

Rot*_*sor 45

您可以使用类型类约束的类型变量而不是IO来使代码可测试.例如,您可以这样做:

{-# LANGUAGE FlexibleInstances #-}
import qualified Prelude
import Prelude hiding(readFile)
import Control.Monad.State
Run Code Online (Sandbox Code Playgroud)

稍后,您可以在IO中运行它:

class Monad m => FSMonad m where
    readFile :: FilePath -> m String

-- | 4) Counts the number of characters in a file
numCharactersInFile :: FSMonad m => FilePath -> m Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)
Run Code Online (Sandbox Code Playgroud)

并测试它:

instance FSMonad IO where
    readFile = Prelude.readFile
Run Code Online (Sandbox Code Playgroud)

如您所见,这种方式您的测试代码需要最少量的修改.

完整的代码可以在这里找到:http://hpaste.org/51210

  • 这是IMO唯一正确的方法.遗憾的是,这些不是一组标准的可模拟类型类,以及标准的可模拟性. (5认同)

oli*_*ver 19

正如Alexander Poluektov已经指出的那样,您尝试测试的代码很容易被分成纯粹和不纯的部分.不过我觉得知道如何在haskell中测试这些不纯的函数是件好事.
在haskell中测试的常用方法是使用quickcheck,这也是我也倾向于使用不纯的代码.

下面是一个示例,说明如何实现您尝试执行的操作,从而为您提供一种模拟行为*:

import Test.QuickCheck
import Test.QuickCheck.Monadic(monadicIO,run,assert)
import System.Directory(removeFile,getTemporaryDirectory)
import System.IO
import Control.Exception(finally,bracket)

numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)
Run Code Online (Sandbox Code Playgroud)

现在提供一个替代功能(针对模型进行测试):

numAlternative ::  FilePath -> IO Integer
numAlternative p = bracket (openFile p ReadMode) hClose hFileSize
Run Code Online (Sandbox Code Playgroud)

为测试环境提供任意实例:

data TestFile = TestFile String deriving (Eq,Ord,Show)
instance Arbitrary TestFile where
  arbitrary = do
    n <- choose (0,2000)
    testString <- vectorOf n $ elements ['a'..'z'] 
    return $ TestFile testString
Run Code Online (Sandbox Code Playgroud)

针对模型的属性测试(使用monadic代码的快速检查):

prop_charsInFile (TestFile string) = 
  length string > 0 ==> monadicIO $ do
    (res,alternative) <- run $ createTmpFile string $
      \p h -> do
          alternative <- numAlternative p
          testRes <- numCharactersInFile p
          return (testRes,alternative)
    assert $ res == fromInteger alternative
Run Code Online (Sandbox Code Playgroud)

还有一点辅助功能:

createTmpFile :: String -> (FilePath -> Handle -> IO a) -> IO a
createTmpFile content func = do
      tempdir <- catch getTemporaryDirectory (\_ -> return ".")
      (tempfile, temph) <- openTempFile tempdir ""
      hPutStr temph content
      hFlush temph
      hClose temph
      finally (func tempfile temph) 
              (removeFile tempfile)
Run Code Online (Sandbox Code Playgroud)

这将让quickCheck为您创建一些随机文件,并根据模型函数测试您的实现.

$ quickCheck prop_charsInFile 
+++ OK, passed 100 tests.
Run Code Online (Sandbox Code Playgroud)

当然,您也可以根据您的用例测试其他一些属性.


*请注意我对术语模拟行为的使用:面向对象意义上
的术语mock可能不是最好的.但模拟背后的意图是什么?
它让你测试需要访问通常是的资源的代码

  • 要么在测试时不可用
  • 或者不容易控制,因此不容易验证.

通过将提供这样的资源的责任转移到快速检查,突然变得可行的是为测试后的代码提供可以在测试运行之后验证的环境.
马丁福勒在一篇关于嘲讽文章中很好地描述了这一点:
"嘲笑......预先编程了预期的对象,形成了他们期望接收的电话的规范."
对于快速检查设置,我会说作为输入生成的文件是"预编程的",以便我们知道它们的大小(==期望).然后根据我们的规范(== property)验证它们.

  • 这不会像嘲笑那样行动,说话或说话.模拟不应该触及文件系统.我认为OP可能需要引入类型类来抽象IO的使用以便使用模拟对象,就像任何其他语言一样.即在C++中需要虚函数,在Java中,接口需要使用模拟框架. (10认同)

Ank*_*kur 9

为此,您需要修改该函数,使其成为:

numCharactersInFile :: (FilePath -> IO String) -> FilePath -> IO Int
numCharactersInFile reader fileName = do
                         contents <- reader fileName
                         return (length contents)
Run Code Online (Sandbox Code Playgroud)

现在您可以传递任何带有文件路径的模拟函数并返回IO字符串,例如:

fakeFile :: FilePath -> IO String
fakeFile fileName = return "Fake content"
Run Code Online (Sandbox Code Playgroud)

并将此功能传递给numCharactersInFile.

  • +1:回答问题,但看起来非常不是Haskell-ish IMO (2认同)

Ale*_*tov 8

该函数由两部分组成:impure(读取部分内容为String)和pure(计算String的长度).

根据定义,不纯的部分不能"单位"测试.纯粹的部分只是调用库函数(当然你可以测试它,如果你想:)).

因此,在这个例子中没有什么可以模拟,也没有什么可以进行单元测试.

换句话说.考虑您有一个相同的C++或Java实现(*):读取内容然后计算其长度.你真的想要嘲笑什么以及之后测试的内容是什么?


(*)这当然不是你在C++或Java中的方式,但这是offtopic.

  • 我没有投票,因为这不是答案.这个答案可以概括为"你不能",这不是真的,因此没用.此外,我没有看到单元测试的定义不允许测试不纯函数(他们在Java中一直这样做:D). (4认同)

Dav*_*vid 6

基于我对外行人对Haskell的理解,我得出以下结论:

  1. 如果函数使用IO monad,那么模拟测试将是不可能的.避免在函数中对IO monad进行硬编码.

  2. 制作函数的辅助版本,其中包含可能执行IO的其他函数.结果将如下所示:

numCharactersInFile' :: Monad m => (FilePath -> m String) -> FilePath -> m Int
numCharactersInFile' f filePath = do
    contents <- f filePath
    return (length contents)
Run Code Online (Sandbox Code Playgroud)

numCharactersInFile' 现在可以用模拟测试了!

mockFileSystem :: FilePath -> Identity String
mockFileSystem "fileName" = return "mock file contents"
Run Code Online (Sandbox Code Playgroud)

现在你可以验证numCharactersInFile'返回没有IO的预期结果:

18 == (runIdentity .  numCharactersInFile' mockFileSystem $ "fileName")
Run Code Online (Sandbox Code Playgroud)

最后,导出原始函数签名的一个版本以与IO一起使用

numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile
Run Code Online (Sandbox Code Playgroud)

因此,在一天结束时,numCharactersInFile'可以使用模拟进行测试.numCharactersInFile只是numCharactersInFile'的变体.