是否可以测试Haskell I / O函数的返回值?

时间:2009-12-20 21:23:30

标签: unit-testing haskell functional-programming

Haskell是一种纯函数式语言,这意味着Haskell函数没有副作用。 I / O使用代表I / O计算块的monad实现。

是否可以测试Haskell I / O函数的返回值?

假设我们有一个简单的'hello world'计划:

main :: IO ()
main = putStr "Hello world!"

我是否可以创建一个可以运行main的测试工具并检查I / O monad是否返回正确的'值'?或者monad被认为是不透明的计算块会阻止我这样做吗?

注意,我不是要比较I / O操作的返回值。 我想比较I / O函数的返回值 - I / O monad本身

因为在Haskell中返回而不是执行I / O,所以我希望检查I / O函数返回的I / O计算块,看看它是否正确。我认为这可以允许I / O函数以I / O是副作用的命令式语言中的单元测试方式进行单元测试。

5 个答案:

答案 0 :(得分:8)

我这样做的方法是创建我自己的IO monad,其中包含我想要建模的动作。我会在我的monad中运行我想要比较的monadic计算并比较它们的效果。

我们举一个例子。假设我想模拟打印的东西。然后我可以这样建模我的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

有些东西是这种模型无法处理的。例如,输入很棘手。但我希望它适合你的用例。我还要提一下,以这种方式建模效果有更聪明有效的方法。我选择了这种特殊方式,因为我认为这是最容易理解的方式。

有关更多信息,我可以推荐论文“野兽之美:尴尬小队的功能语义”,可以在this page上找到一些其他相关论文。

答案 1 :(得分:4)

在IO monad中,您可以测试IO功能的返回值。要测试IO monad之外的返回值是不安全的:这意味着它可以完成,但只会有破坏程序的风险。仅供专家使用。

值得注意的是,在您显示的示例中,main的值具有类型IO (),这意味着“我是一个IO动作,执行时会执行一些I / O然后返回()类型的值。“类型()发音为“unit”,并且此类型只有两个值:空元组(也写为()和发音为“unit”)和“bottom”,这是Haskell的名称不终止或出错的计算。

值得指出的是,测试 IO monad中的IO函数的返回值是非常简单和正常的,并且通过使用do表示法来实现它的惯用方法

答案 2 :(得分:1)

您可以使用QuickCheck 2测试某些 monadic代码。自从我阅读这篇论文以来已经很长时间了,所以我不记得它是否适用于IO动作或它可以应用于哪种monadic计算。此外,您可能会发现很难将单元测试表示为QuickCheck属性。尽管如此,作为一个非常满意的QuickCheck用户,我会说它比很多更好,而不是无所事事,或者只是用unsafePerformIO进行攻击。

答案 3 :(得分:0)

我很遗憾地告诉你,你不能这样做。

unsafePerformIO基本上让你完成这个。但我强烈希望你使用它。

Foreign.unsafePerformIO :: IO a -> a

:/

答案 4 :(得分:0)

我喜欢this answer关于SO的类似问题及其评论。基本上,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将足够聪明,可以完全跳过它发现的任何虚假断言,而实际的断言肯定会在失败时使程序崩溃。

这是IMO,不太可能 - 你仍然需要在复杂场景中进行行为测试 - 但我想这可以帮助检查代码所做的基本假设是否正确。