Unit testing 是否可以测试Haskell I/O函数的返回值?
Haskell是一种纯函数语言,这意味着Haskell函数没有副作用。I/O是使用表示I/O计算块的单子实现的 是否可以测试Haskell I/O函数的返回值? 假设我们有一个简单的“hello world”程序:Unit testing 是否可以测试Haskell I/O函数的返回值?,unit-testing,haskell,functional-programming,Unit Testing,Haskell,Functional Programming,Haskell是一种纯函数语言,这意味着Haskell函数没有副作用。I/O是使用表示I/O计算块的单子实现的 是否可以测试Haskell I/O函数的返回值? 假设我们有一个简单的“hello world”程序: main :: IO () main = putStr "Hello world!" 我是否可以创建一个可以运行main的测试线束,并检查I/O monad it是否返回正确的“值”?或者单子应该是不透明的计算块这一事实阻止了我这么做吗 注意,我没有尝试比较I/O操作的返回值我想比
main :: IO ()
main = putStr "Hello world!"
我是否可以创建一个可以运行main
的测试线束,并检查I/O monad it是否返回正确的“值”?或者单子应该是不透明的计算块这一事实阻止了我这么做吗
注意,我没有尝试比较I/O操作的返回值我想比较I/O函数的返回值—I/O单子本身
因为在Haskell中,I/O是返回的,而不是执行的,所以我希望检查I/O函数返回的I/O计算块,看看它是否正确。我认为这可以让I/O函数以一种在命令式语言中无法实现的方式进行单元测试,而在命令式语言中,I/O是一种副作用。我很抱歉地告诉您,您不能这样做
unsafePerformIO
基本上让我们来完成这个。但我强烈希望你不要使用它
Foreign.unsafePerformIO :: IO a -> a
:/在IO单子中,您可以测试IO函数的返回值。在IO monad之外测试返回值是不安全的:这意味着可以这样做,但只会有破坏程序的风险。仅供专家使用 值得注意的是,在您展示的示例中,
main
的值具有类型IO()
,这意味着“我是一个IO操作,当执行时,执行一些I/O,然后返回类型()
”的值。类型()
的发音为“unit”,这种类型的值只有两个:空元组(也写为()
,发音为“unit”)和“bottom”,这是Haskell对不会终止或出错的计算的称呼
值得指出的是,从IO monad内部测试IO函数的返回值是非常简单和正常的,惯用的方法是使用do
表示法。我喜欢一个类似的问题及其注释。基本上,IO通常会产生一些变化,这些变化可能会从外部注意到您的测试需要与该更改是否正确有关(例如,生成了正确的目录结构等)
基本上,这意味着“行为测试”,在复杂的情况下,这可能是一个相当痛苦的问题。这就是为什么您应该将代码中特定于IO的部分保持在最低限度,并将尽可能多的逻辑移到纯(因此非常容易测试)函数的部分原因
同样,您可以使用断言函数:
actual_assert :: String -> Bool -> IO ()
actual_assert _ True = return ()
actual_assert msg False = error $ "failed assertion: " ++ msg
faux_assert :: String -> Bool -> IO ()
faux_assert _ _ = return ()
assert = if debug_on then actual_assert else faux_assert
(您可能希望在构建脚本之前构建的单独模块中定义debug_on
。此外,如果不是标准库,Hackage上的软件包很可能会以更精细的形式提供这一点……如果有人知道这样的工具,请编辑此帖子/评论,以便我可以编辑。)
我认为GHC将足够聪明,完全跳过它发现的任何虚假断言,而实际断言肯定会在失败时使您的程序崩溃
在我看来,这不太可能足够——你仍然需要在复杂的场景中进行行为测试——但我想这有助于检查代码所做的基本假设是否正确。我这样做的方法是创建我自己的IO monad,其中包含我想要建模的动作。我将运行monadic我想在我的monad中比较计算结果,并比较它们的效果 让我们举一个例子。假设我想对打印内容建模。然后我可以像这样对IO monad建模:
data IO a where
Return :: a -> IO a
Bind :: IO a -> (a -> IO b) -> IO b
PutChar :: Char -> IO ()
instance Monad IO where
return a = Return a
Return a >>= f = f a
Bind m k >>= f = Bind m (k >=> f)
PutChar c >>= f = Bind (PutChar c) f
putChar c = PutChar c
runIO :: IO a -> (a,String)
runIO (Return a) = (a,"")
runIO (Bind m f) = (b,s1++s2)
where (a,s1) = runIO m
(b,s2) = runIO (f a)
runIO (PutChar c) = ((),[c])
以下是我将如何比较效果:
compareIO :: IO a -> IO b -> Bool
compareIO ioA ioB = outA == outB
where ioA = runIO ioA ioB
有些事情是这种模型无法处理的。例如,输入是很棘手的。但我希望它能适合您的使用情况。我还应该提到,有更聪明、更有效的方法可以用这种方式对效果进行建模。我选择这种方法是因为我认为它是最容易理解的方法
关于更多信息,我可以推荐论文《野兽之美:尴尬团队的功能语义学》你可以用它来测试一些一元代码。我已经很久没有读过这篇文章了,所以我不记得它是否适用于IO操作或者它可以应用于什么类型的一元计算。另外,你可能发现很难将你的单元测试表示为QuickCheck属性不过,作为一个非常满意的QuickCheck用户,我会说它比什么都不做或使用
unsafePerformIO
好得多。为什么不呢:test=main>=\d->return$\u test\u value\ud
?因此,在我的测试代码中不能使用unsafePerformIO,但在我的生产代码中不能使用unsafePerformIO来编程测试IO?@ctford:我真正建议的是让您的测试代码在IO monad中工作。monad不一定是“不透明”的计算块。例如,列表和可能的monad都有应用程序可见的行为-IO monad是唯一一个(我知道的)专门设计用于将程序逻辑与其某些行为隔离开来。Control.Monad.ST也相当不透明。我对计算理论方面的东西不太在行。但这不等于停止问题吗?我的意思是,你会有一个返回程序的函数(IO操作)我想写另一个程序来静态分析它的正确性。这是描述问题的有效方法吗?这是可判定的吗?我希望测试IO操作本身是正确的,而不是操作返回的结果(正如你正确指出的,在这种情况下,它什么都不是)。我希望区分