立即通过真实世界Haskell。这是本书中早期练习的解决方案:
-- | 4) Counts the number of characters in a file
numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
contents <- readFile fileName
return (length contents)
我的问题是:你如何测试这个功能?有没有办法制作“模拟”输入而不是实际需要与文件系统进行交互来测试它? Haskell强调纯函数,我不得不想象这很容易做到。
答案 0 :(得分:43)
您可以使用类型类约束的类型变量而不是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)
稍后,您可以在IO中运行它:
instance FSMonad IO where
readFile = Prelude.readFile
并测试它:
data MockFS = SingleFile FilePath String
instance FSMonad (State MockFS) where
-- ^ Reader would be enough in this particular case though
readFile pathRequested = do
(SingleFile pathExisting contents) <- get
if pathExisting == pathRequested
then return contents
else fail "file not found"
testNumCharactersInFile :: Bool
testNumCharactersInFile = evalState
(numCharactersInFile "test.txt")
(SingleFile "test.txt" "hello world")
== 11
正如您所看到的,这种方式您正在测试的代码需要最少量的修改。
完整代码可在此处找到:http://hpaste.org/51210
答案 1 :(得分: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)
现在提供一个替代函数(Testing against a model):
numAlternative :: FilePath -> IO Integer
numAlternative p = bracket (openFile p ReadMode) hClose hFileSize
为测试环境提供任意实例:
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
针对模型的属性测试(使用quickcheck for monadic code):
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
还有一个辅助功能:
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)
这将让quickCheck为您创建一些随机文件,并根据模型函数测试您的实现。
$ quickCheck prop_charsInFile
+++ OK, passed 100 tests.
当然,您也可以根据您的用例测试其他一些属性。
* 请注意我对术语模拟行为的使用情况:
面向对象意义上的术语 mock 可能不是最好的。但是模拟背后的意图是什么?
它让你测试需要访问通常是
通过将提供此类资源的责任转移到快速检查,突然变得可行的是为测试后的代码提供可在测试运行后验证的环境。
Martin Fowler在article about mocks中很好地描述了这一点:
“模拟是......预先编程的对象具有预期,这些对象形成了他们期望接收的调用的规范。”
对于快速检查设置,我会说作为输入生成的文件是“预编程的”,以便我们知道它们的大小(==期望)。然后根据我们的规范(== property)验证它们。
答案 2 :(得分:9)
为此,您需要修改该功能,使其成为:
numCharactersInFile :: (FilePath -> IO String) -> FilePath -> IO Int
numCharactersInFile reader fileName = do
contents <- reader fileName
return (length contents)
现在您可以传递任何带有文件路径的模拟函数并返回IO字符串,例如:
fakeFile :: FilePath -> IO String
fakeFile fileName = return "Fake content"
并将此函数传递给numCharactersInFile
。
答案 3 :(得分:8)
该函数由两部分组成:impure(读取部分内容为String)和pure(计算String的长度)。
根据定义,不纯的部分不能被“单位”测试。纯粹的部分只是调用库函数(当然你可以测试它,如果你想:) :)。
所以在这个例子中没有什么可以模拟,也没有什么可以进行单元测试。
换句话说。考虑您有一个相同的C ++或Java实现(*):读取内容然后计算其长度。你真的想要嘲笑什么以及之后测试的内容是什么?
(*)当然不是你将用C ++或Java做的方式,但这是offtopic。
答案 4 :(得分:5)
根据我的外行人对Haskell的理解,我得出以下结论:
如果某个函数使用IO monad,则模拟测试将无法进行。避免在函数中对IO monad进行硬编码。
制作函数的辅助版本,其中包含可能执行IO的其他函数。结果将如下所示:
numCharactersInFile' :: Monad m => (FilePath -> m String) -> FilePath -> m Int numCharactersInFile' f filePath = do contents <- f filePath return (length contents)
numCharactersInFile'
现在可以用模拟测试了!
mockFileSystem :: FilePath -> Identity String
mockFileSystem "fileName" = return "mock file contents"
现在你可以验证numCharactersInFile'返回没有IO的预期结果:
18 == (runIdentity . numCharactersInFile' mockFileSystem $ "fileName")
最后,导出原始函数签名的一个版本以用于IO
numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile
因此,在一天结束时,numCharactersInFile'可以使用模拟进行测试。 numCharactersInFile只是numCharactersInFile'的变体。