Haskell单元测试

88

我刚学 Haskell,现在正在进行单元测试。但是我发现 Haskell 的生态系统非常复杂,我对 HTF 和 HUnit 之间的关系感到困惑。

在某些示例中,你需要设置测试用例,并将它们导出为 tests 列表,然后使用 runTestsTT 在 ghci 中运行(就像这个 HUnit 示例)。

在其他示例中,你需要创建一个与 cabal 文件绑定的测试运行程序,它使用一些预处理器魔法来查找你的测试,就像这个 git 示例。此外,似乎 HTF 测试需要以 test_ 为前缀才能运行?我很难找到任何关于这方面的文档,但我注意到每个人都遵循了这种模式。

无论如何,有人能帮助我整理一下吗?在 Haskell 中什么被认为是标准的做法?最佳实践是什么?哪种方法最容易设置和维护?


你看过 QuickCheck 库吗?我一直觉得它很容易使用。 - bheklilr
4
是的,但快速检查是另一种用例,那是基于类型的测试,而这不是我现在想要的。不过,一旦我理解了htf和hunit的关系,我也会很感兴趣了解如何整合它们。 - devshorts
https://twitter.com/HaskellTips/status/425793151660331008 建议优先使用tasty而不是test-framework(HTF?),但我也看到HTF在几个月的沉寂后上周进行了小更新。 - misterbee
3个回答

60

通常,任何重要的Haskell项目都使用Cabal进行管理。Cabal可以负责构建、分发、文档化(借助haddock)和测试。

标准做法是将测试文件放在test目录下,并在.cabal文件中设置一个测试套件。详见用户手册。这是我某个项目的测试套件的样子:

Test-Suite test-melody
  type:               exitcode-stdio-1.0
  main-is:            Main.hs
  hs-source-dirs:     test
  build-depends:      base >=4.6 && <4.7,
                      test-framework,
                      test-framework-hunit,
                      HUnit,
                      containers == 0.5.*

然后在文件test/Main.hs中。

import Test.HUnit
import Test.Framework
import Test.Framework.Providers.HUnit
import Data.Monoid
import Control.Monad
import Utils

pushTest :: Assertion
pushTest = [NumLit 1] ^? push (NumLit 1)

pushPopTest :: Assertion
pushPopTest = [] ^? (push (NumLit 0) >> void pop)

main :: IO ()
main = defaultMainWithOpts
       [testCase "push" pushTest
       ,testCase "push-pop" pushPopTest]
       mempty

Utils 定义了一些比 HUnit 更友好的接口。

对于轻量级测试,我强烈建议您使用 QuickCheck。它可以让您编写简短的属性并在一系列随机输入上进行测试。例如:

 -- Tests.hs
 import Test.QuickCheck

 prop_reverseReverse :: [Int] -> Bool
 prop_reverseReverse xs = reverse (reverse xs) == xs

然后

 $ ghci Tests.hs
 > import Test.QuickCheck
 > quickCheck prop_reverseReverse
 .... Passed Tests (100/100)

7
随着项目的发展,您需要维护导出的测试列表吗?这似乎容易出错。我猜我仍然不明白这与自动导出的预处理器方法有什么关系?我看到很多单元测试的例子,但它们都不同。 - devshorts
@devshorts 测试列表允许您为每个测试命名。我相信有一些框架可以自动运行您的测试,但我通常每个文件有大约10个测试,因此维护这样小的列表非常容易。 - daniel gratzer
1
你可以详细说明一下如何使用该方法来处理多个测试文件吗?主运行程序是单独的还是通常作为测试装置的一部分? - devshorts
1
@devshorts 我将我的测试拆分,从每个模块中导出带有名称的测试列表。然后在主函数中,我组合这些列表并运行它们。 - daniel gratzer
我使用了这个答案来为这个小项目编写测试框架:https://github.com/siddharthist/m3u-convert 感谢@jozefg,希望这个例子能帮助大家 :-) - Langston
在查看了detailed-0.9上的cabal文档后,我认为这个答案是ghc/cabal的最先进的“我想进行简单的单元测试”设置。我觉得对于这么基础的东西来说,依赖于三个包test-frameworkHUnittest-framework-hunit有点尴尬。而且看起来现在已经这样8年了。 - ruben.moor

37

我也是haskell的新手,我发现这篇介绍非常有帮助: "Getting started with HUnit"。简单来说,我会在这里提供一个使用HUnit进行测试的简单示例,而无需使用.cabal项目文件:

假设我们有一个名为SafePrelude.hs的模块:

module SafePrelude where

safeHead :: [a] -> Maybe a
safeHead []    = Nothing
safeHead (x:_) = Just x

我们可以将测试放在TestSafePrelude.hs中,具体如下:

module TestSafePrelude where

import Test.HUnit
import SafePrelude

testSafeHeadForEmptyList :: Test
testSafeHeadForEmptyList = 
    TestCase $ assertEqual "Should return Nothing for empty list"
                           Nothing (safeHead ([]::[Int]))

testSafeHeadForNonEmptyList :: Test
testSafeHeadForNonEmptyList =
    TestCase $ assertEqual "Should return (Just head) for non empty list" (Just 1)
               (safeHead ([1]::[Int]))

main :: IO Counts
main = runTestTT $ TestList [testSafeHeadForEmptyList, testSafeHeadForNonEmptyList]

现在使用ghc运行测试非常容易:

runghc TestSafePrelude.hs

或者 hugs - 在这种情况下,TestSafePrelude.hs 必须重命名为 Main.hs(据我所知,对于 hugs 来说是这样)(不要忘记修改模块头):

runhugs Main.hs

或者任何其他 haskell 编译器 ;-)

当然,在 HUnit 中还有更多的内容,所以我强烈建议阅读建议的教程和库 用户指南


1
你好,HUnit入门链接已经失效。 - sandwood
看起来链接已经修复,现在引用的是页面的存档版本。 - Zev Averbach

8
你已经得到了大部分问题的答案,但你还问了关于HTF以及它是如何工作的。 HTF 是一个框架,旨在进行单元测试 -- 它与 HUnit 兼容(集成并包装它以提供额外的功能) -- 以及基于属性的测试 -- 它与 quickcheck 集成。它使用预处理器来定位测试,因此您不必手动构建列表。预处理器通过 pragma 添加到您的测试源文件中:
{-# OPTIONS_GHC -F -pgmF htfpp #-}

另外,你可以将相同的选项添加到cabal文件中的ghc-options属性中,但我从未尝试过,不知道是否有用。

预处理器会扫描你的模块,寻找命名为test_xxxxprop_xxxx的顶级函数,并将它们添加到模块的测试列表中。你可以直接使用此列表,在模块中放置一个main函数并运行它们(main = htfMain htf_thisModuleTests),也可以将它们导出到模块外,建立一个主测试程序,用于多个模块,导入带有测试的模块并运行所有测试:

import {-@ HTF_TESTS @-} ModuleA
import {-@ HTF_TESTS @-} ModuleB
main :: IO ()
main = htfMain htf_importedTests

这个程序可以使用@jozefg描述的技术与cabal集成,也可以加载到ghci中进行交互式运行(但不适用于Windows - 有关详细信息,请参见https://github.com/skogsbaer/HTF/issues/60)。

Tasty是另一种提供集成不同类型测试方法的方式的替代品。它没有像HTF那样的预处理器,但有一个模块使用Template Haskell执行类似的功能。与HTF一样,它还依赖于命名约定来识别您的测试(在这种情况下,使用case_xxxx而不是test_xxxx)。除了HUnit和QuickCheck测试外,它还有处理其他许多测试类型的模块。


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