Testing 在Haskell中测试执行IO的函数
现在就在现实世界中工作。以下是本书中一个非常早期练习的解决方案:Testing 在Haskell中测试执行IO的函数,testing,haskell,Testing,Haskell,现在就在现实世界中工作。以下是本书中一个非常早期练习的解决方案: -- | 4) Counts the number of characters in a file numCharactersInFile :: FilePath -> IO Int numCharactersInFile fileName = do contents <- readFile fileName return (length contents) -->4)统计文件中的字符数 numCha
-- | 4) Counts the number of characters in a file
numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
contents <- readFile fileName
return (length contents)
-->4)统计文件中的字符数
numCharactersInFile::FilePath->IO Int
numCharactersInFile fileName=do
contents该函数由两部分组成:pupure(将部分内容读取为字符串)和pure(计算字符串长度)
不纯部分不能按定义进行“单位”测试。纯部分只是对库函数的调用(当然,如果需要,您可以测试它:)
因此,在这个例子中,没有什么可模仿的,也没有什么可单元测试的
换一种说法。考虑到你有一个相等的C++或java实现(*):读取内容,然后计算其长度。您真正想要模拟的是什么,以及在以后的测试中还剩下什么
(*),当然不是C++或java中的方式,但这是个错误。
因为你需要修改函数,使它变成:
numCharactersInFile :: (FilePath -> IO String) -> FilePath -> IO Int
numCharactersInFile reader fileName = do
contents <- reader fileName
return (length contents)
并将此函数传递给numCharactersInFile
,正如Alexander Poluektov已经指出的,您尝试测试的代码可以很容易地分为纯代码和不纯代码。
尽管如此,我认为知道如何在haskell中测试这些不纯函数是件好事。
在haskell中测试的通常方法是使用,这也是我在处理不纯净代码时倾向使用的方法
下面是一个例子,说明你如何实现你想要做的事情,这给了你一种模仿的行为*:
为测试环境提供任意实例:
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
当然,您还可以根据您的用例测试一些其他属性
*请注意我对术语模拟行为的用法:
面向对象意义上的mock这个术语在这里可能不是最好的。但是模仿背后的意图是什么呢?
它让您可以测试需要访问资源的代码,这些资源通常是
- 在测试时不可用
- 或者不容易控制,因此不容易验证
通过将提供此类资源的责任转移到quickcheck,为测试中的代码提供一个在测试运行后可以验证的环境突然变得可行。
Martin Fowler在一篇文章中很好地描述了这一点:
“模拟是……预先编程的对象,具有预期,形成预期接收呼叫的规范。”
对于quickcheck设置,我认为作为输入生成的文件是“预编程”的,这样我们就知道它们的大小(=期望值)。然后根据我们的规范(=属性)对它们进行验证。您可以通过使用类型类约束的类型变量而不是IO使代码可测试
首先,让我们把进口货物放在一边
{-# LANGUAGE FlexibleInstances #-}
import qualified Prelude
import Prelude hiding(readFile)
import Control.Monad.State
我们要测试的代码:
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)
并对其进行测试:
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
data MockFS=SingleFile文件路径字符串
实例FSMonad(状态MockFS),其中
--^但在这种情况下,读者就足够了
readFile pathRequested=do
(单文件路径现有内容)根据我的门外汉对Haskell的理解,我得出以下结论:
如果函数使用IO单子,模拟测试将是不可能的。避免在函数中硬编码IO monad
为您的函数创建一个助手版本,该版本将接收可能执行IO的其他函数。结果如下所示:
现在,您可以验证numCharactersInFile'是否返回不带IO的预期结果:
18 == (runIdentity . numCharactersInFile' mockFileSystem $ "fileName")
最后,导出原始函数签名的版本以用于IO
numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile
因此,归根结底,numCharactersInFile’是可以用mock进行测试的。numCharactersInFile只是numCharactersInFile的一个变体。在“模拟”文件系统之后,还有什么需要测试的<代码>长度
函数?Haskell可能强调纯度,但IO单子并不纯净。如果您对文件内容做了比length
更有趣的事情,您可以轻松测试该函数,对于某些a
@Dietrich是的。它的unsafePerformIO
不纯;)@Dietrich No.>>=和>>以及return都是纯的。将IO
替换为Monad m=>m
,它就变得足够哈斯克尔式了。这是IMO唯一正确的方法。不幸的是,这不是一套标准的可模仿类型类,以及标准的可模仿性。同意。解决“真实”模拟的最佳尝试。此解决方案使用TypeSynonym实例。Dave,这个扩展只在brewity的测试代码中使用,你可以通过创建一个newtype
轻松避免。我知道我在问一个noob问题(我是noob haskeller),但我试着不使用语言扩展,我无法让它工作,你能再分享一遍代码吗?我投了反对票,因为这不是答案。这个答案可以概括为“你不能”,这不是真的,因此也没有用。另外,我不明白单元测试的定义是什么,不允许测试不纯净的函数(在Java:D中,它们一直都这样做)。关键的见解是正确的——分别测试纯部分和不纯部分。readFile
结果的“模拟”只是一个字符串——这正是您首先传递给length
的内容!我不同意的是,人们当然可以进行单元测试readFile
——只是这样做时,人们当然不应该嘲笑它。@sclv总之,这取决于你对“单元测试”的定义。我使用了一个禁止触摸做IO事情的函数。FWIW,现在的问题不包含“单元”这个词。@steamer25:但下面的文本暗示了这一点:如果他/她做(非单元)测试,为什么要嘲笑某个东西?这不起作用,也不说话,也不可能
$ quickCheck prop_charsInFile
+++ OK, passed 100 tests.
{-# LANGUAGE FlexibleInstances #-}
import qualified Prelude
import Prelude hiding(readFile)
import Control.Monad.State
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)
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
numCharactersInFile' :: Monad m => (FilePath -> m String) -> FilePath -> m Int
numCharactersInFile' f filePath = do
contents <- f filePath
return (length contents)
mockFileSystem :: FilePath -> Identity String
mockFileSystem "fileName" = return "mock file contents"
18 == (runIdentity . numCharactersInFile' mockFileSystem $ "fileName")
numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile