Haskell 单元测试

我是 Haskell 的新手,从事单元测试工作,但是我发现生态系统非常令人困惑。我对 HTF 和 HUnit 之间的关系感到困惑。

在一些示例中,我看到您设置了测试用例,将它们导出到一个测试列表中,然后使用带有 runTestsTT的 ghci 运行(如 这个 HUnit 例子)。

在其他示例中,您将创建一个绑定到 cabal 文件的测试运行程序,该文件使用一些预处理器魔法来查找您的测试,如本 举个例子所示。另外,HTF 测试似乎需要以 test_作为前缀,否则它们就不会运行?我好不容易才找到这方面的文件,我只是注意到了每个人的模式。

不管怎样,有人能帮我解决这个问题吗?哈斯克尔的标准做事方式是什么?最佳实践是什么?什么是最容易设置和维护的?

29745 次浏览

Generally, any significant Haskell project is run with Cabal. This takes care of building, distribution, documentation (with the help of haddock), and testing.

The standard approach is to put your tests in the test directory and then set up a test suite in your .cabal file. This is detailed in the user manual. Here's what the test suite for one of my projects looks like

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.*

Then in the file 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

Where Utils defines some nicer interfaces over HUnit.

For lighter-weight testing, I strongly recommend you use QuickCheck. It lets you write short properties and test them over a series of random inputs. For example:

 -- Tests.hs
import Test.QuickCheck


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

And then

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

I'm also newbie haskeller and I've found this introduction really helpful: "Getting started with HUnit". To summarize, I'll put here simple testing example of HUnit usage without .cabal project file:

Let's assume that we have module SafePrelude.hs:

module SafePrelude where


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

we can put tests into TestSafePrelude.hs as follows:

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]

Now it's easy to run tests using ghc:

runghc TestSafePrelude.hs

or hugs - in this case TestSafePrelude.hs has to be renamed to Main.hs (as far as I'm familiar with hugs) (don't forget to change module header too):

runhugs Main.hs

or any other haskell compiler ;-)

Of course there is more then that in HUnit, so I really recommend to read suggested tutorial and library User's Guide.

You've had answers to most of your questions, but you also asked about HTF, and how that works.

HTF is a framework that is designed for both unit testing -- it is backwards compatible with HUnit (it integrates and wraps it to provide extra functions) -- and property-based testing -- it integrates with quickcheck. It uses a preprocessor to locate tests so that you don't have to manually build a list. The preprocessor is added to your test source files using a pragma:

{-# OPTIONS_GHC -F -pgmF htfpp #-}

(alternatively, I guess you could add the same options to your ghc-options property in your cabal file, but I've never tried this so don't know if it is useful or not).

The preprocessor scans your module for top-level functions named test_xxxx or prop_xxxx and adds them to a list of tests for the module. You can either use this list directly by putting a main function in the module and running them (main = htfMain htf_thisModuleTests) or export them from the module, and have a main test program for multiple modules, which imports the modules with tests and runs all of them:

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

This program can be integrated with cabal using the technique described by @jozefg, or loaded into ghci and run interactively (although not on Windows - see https://github.com/skogsbaer/HTF/issues/60 for details).

Tasty is another alternative that provides a way of integrating different kinds of tests. It doesn't have a preprocessor like HTF, but has a module that performs similar functions using Template Haskell. Like HTF, it also relies on naming convention to identify your tests (in this case, case_xxxx rather than test_xxxx). In addition to HUnit and QuickCheck tests, it also has modules for handling a number of other test types.