如何在Haskell中测试错误?

9

我希望能够确保当函数接收到无效值时,它会抛出一个错误。例如,假设我有一个函数pos,它只返回一个正数:

pos :: Int -> Int
pos x
   | x >= 0 = x
   | otherwise = error "Invalid Input"

这只是一个简单的例子,但我希望您能理解其中的意义。

我想编写一个测试用例,可以预期出错并将其视为一项通过的测试。例如:

tests = [pos 1 == 1, assertError pos (-1), pos 2 == 2, assertError pos (-2)]
runTests = all (== True) tests

[我的方案]

根据@hammar的评论,这是我最终选择的方案。

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

assertException :: (Exception e, Eq e) => e -> IO a -> IO ()
assertException ex action =
    handleJust isWanted (const $ return ()) $ do
        action
        assertFailure $ "Expected exception: " ++ show ex
  where isWanted = guard . (== ex) 

assertError ex f = 
    TestCase $ assertException (ErrorCall ex) $ evaluate f

tests = TestList [ (pos 0) ~?= 0
                 , (pos 1) ~?= 1
                 , assertError "Invalid Input" (pos (-1))
                 ]   

main = runTestTT tests

3
error语句会抛出一个ErrorCall异常。请参考我在这里的回答,了解如何使用HUnit测试异常。 - hammar
@hammar 啊,我不确定那在这种情况下是否有效。你考虑过将其提交给 HUnit 项目吗?如果能够内置该功能会很好。 - Joe Hillenbrand
2
(注意 all (== True) 等于 all id 等于 and。) - huon
2个回答

4

OP的解决方案定义了assertException,但看起来testpack中的Test.HUnit.Tools.assertRaises也可以在这里使用。

我添加了msg参数以匹配assertRaises的工作方式,并包括有选择性的导入,以便像我这样的新手可以了解常用的导入位置。

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

pos :: Int -> Int
pos x
   | x >= 0 = x
   | otherwise = error "Invalid Input"

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

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

tests = TestList [
  (pos 0) ~?= 0
  , (pos 1) ~?= 1
  , assertError "Negative argument raises an error" "Invalid Input" (pos (-1))
  ]   

main = runTestTT tests

我喜欢这个答案!但是你有没有其他实现方法的想法?你在这里使用的HUnit Tools库已经好几年没有维护了。 - Lukas Süsslin

0

处理Haskell中的错误有几种方法。这是一个概述:http://www.randomhacks.net/articles/2007/03/10/haskell-8-ways-to-report-errors

[编辑]

第一个示例显示了如何捕获错误,例如

half :: Int -> Int 
half x = if even x then x `div` 2 else error "odd"

main = do catch (print $ half 23) (\err -> print err)

话虽如此,这种类型的错误处理更适合于像你的纯代码中的IO等情况。也许使用"Maybe"、"Either"或类似的东西会是一个更好的选择。可以简单地做到...

pos :: Int -> Maybe Int
pos x
   | x >= 0 = Just x
   | otherwise = Nothing

tests = [pos 1 == Just 1
        ,pos (-1) == Nothing
        ,pos 2 == Just 2
        ,pos (-2) == Nothing
        ]

main = print $ and tests

如果您不需要错误类型。


一个有用的链接,但它并没有回答如何测试错误的问题。 - opqdonut
这个答案的问题在于它要求我更改我的函数以适应测试,这并不是测试的真正目的。我希望我的函数保持不变。 - Joe Hillenbrand
如果你的函数是纯函数,它们应该尽可能避免调用 error。如果它们在 IO 中,catch 可以很好地工作。如果这不能解决问题,可以编写一个包装函数将错误转换为 Maybe 或其他类型。我猜为了测试目的,甚至可以使用臭名昭著的 unsafePerformIO - Landei

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接