Haskell异常和单元测试

时间:2013-02-10 21:12:06

标签: unit-testing exception haskell

基于SO问题13350164 How do I test for an error in Haskell?,我正在尝试编写一个单元测试,断言给定无效输入,递归函数会引发异常。我采用的方法适用于非递归函数(或者当第一个调用引发异常时),但是一旦异常发生在调用链中,断言就会失败。

我已经阅读了问题6537766 Haskell approaches to error handling的优秀答案,但不幸的是,对于我的学习曲线的这一点,这个建议有点过于通用。我的猜测是这里的问题与懒惰评估和非纯测试代码有关,但我很欣赏专家解释。

我是否应该在这种情况下采用不同的方法来处理错误(例如MaybeEither),或者在使用此样式时是否有合理的解决办法使测试用例正常工作? / p>

这是我提出的代码。前两个测试用例成功,但第三个测试用例失败了"Received no exception, but was expecting exception: Negative item"

import Control.Exception (ErrorCall(ErrorCall), evaluate)
import Test.HUnit.Base  ((~?=), Test(TestCase, TestList))
import Test.HUnit.Text (runTestTT)
import Test.HUnit.Tools (assertRaises)

sumPositiveInts :: [Int] -> Int
sumPositiveInts [] = error "Empty list"
sumPositiveInts (x:[]) = x
sumPositiveInts (x:xs) | x >= 0 = x + sumPositiveInts xs
                       | otherwise = error "Negative item"

instance Eq ErrorCall where
    x == y = (show x) == (show y)

assertError msg ex f = 
    TestCase $ assertRaises msg (ErrorCall ex) $ evaluate f

tests = TestList [
  assertError "Empty" "Empty list" (sumPositiveInts ([]))
  , assertError "Negative head" "Negative item" (sumPositiveInts ([-1, -1]))
  , assertError "Negative second item" "Negative item" (sumPositiveInts ([1, -1]))
  ]   

main = runTestTT tests

1 个答案:

答案 0 :(得分:7)

sumPositiveInts实际上只是一个错误。当唯一的负数是列表中的最后一个 - 第二个分支不包含检查时,您的代码进行否定性检查。

值得注意的是,像你这样编写递归的规范方法会打破“空虚”测试,以避免这个错误。通常,将您的解决方案分解为“总和”加上两个警卫将有助于避免错误。


顺便说一句,我是Haskell approaches to error handling的建议。 Control.Exception更难以推理和学习,error只应用于标记无法实现的代码分支 - 我更喜欢一些应该被称为{{1}的建议}。

为了使建议有形,我们可以使用impossible重建此示例。首先,无人看守的功能是内置的:

Maybe

然后我们需要构建两个警卫(1)空列表给sum :: Num a => [a] -> a 和(2)包含负数的列表给Nothing

Nothing

我们可以将它们组合成一个monad

emptyIsNothing :: [a] -> Maybe [a]
emptyIsNothing [] = Nothing
emptyIsNothing as = Just as

negativeGivesNothing :: [a] -> Maybe [a]
negativeGivesNothing xs | all (>= 0) xs = Just xs
                        | otherwise     = Nothing

然后我们可以使用很多习语和缩减来使这些代码更容易阅读和编写(一旦你知道约定!)。让我强调,在此之后没有必要是必要的,也不是非常容易理解。学习它可以提高你分解函数的能力,并且流利地考虑FP,但我只是跳到高级的东西。

例如,我们可以使用“Monadic sumPositiveInts :: [a] -> Maybe a sumPositiveInts xs = do xs1 <- emptyIsNothing xs xs2 <- negativeGivesNothing xs1 return (sum xs2) ”(也称为Kleisli箭头组合)来编写(.)

sumPositiveInts

我们可以使用组合器

简化sumPositiveInts :: [a] -> Maybe a sumPositiveInts = emptyIsNothing >=> negativeGivesNothing >=> (return . sum) emptyIsNothing
negativeGivesNothing

如果任何包含的值为elseNothing :: (a -> Bool) -> a -> Just a pred `elseNothing` x | pred x = Just x | otherwise = Nothing emptyIsNothing = elseNothing null negativeGivesNothing = sequence . map (elseNothing (>= 0)) sequence :: [Maybe a] -> Maybe [a]将失败整个列表。我们实际上可以更进一步,因为Nothing是一个常见的习语

sequence . map f

所以,最后

negativeGivesNothing = mapM (elseNothing (>= 0))