8

我有一堆函数,比如:method1, method2, method3. 对于所有这些都有HUnit测试功能,例如:testMethod1, testMethod2, testMethod3.

testMethod1 = TestCase $
  assertEqual "testmethod1" ...

testMethod2 = TestCase $
  assertEqual "testmethod2" ...

testMethod3 = TestCase $
  assertEqual "testmethod3" ...

我想避免将函数名称作为错误消息的前缀进行冗余复制,并将其称为:

testMethod1 = TestCase $
  assertEqual_ ...

如何实现(赞赏任何“魔术”技巧)?

所以实际上的问题是如何在它的定义中使用函数名称?


更新

从原始问题中实际上并不清楚,我也想处理这种情况:

tProcess = TestCase $ do
  assertEqual "tProcess" testResult $ someTest
  assertEqual "tProcess" anotherTestResult $ anotherTest
  assertEqual "tProcess" resultAgain $ testAgain

最后我想写这样的东西:

tProcess = TestCase $ do
  assertEqual_ testResult $ someTest
  assertEqual_ anotherTestResult $ anotherTest
  assertEqual_ resultAgain $ testAgain
4

2 回答 2

11

您不能直接执行此操作(即,您的测试用例以 开头testMethodN = ...),但您可以使用Template Haskell来获得:

testCase "testMethod1" [| do
    assertEqual_ a b
    assertEqual_ c d
 |]

这涉及编写testCase :: String -> Q Exp -> Q [Dec],将测试用例的名称和引用的表达式转换为声明列表的函数。例如:

{-# LANGUAGE TemplateHaskell #-}
    
import Data.Char
import Control.Applicative
import Control.Monad
import Language.Haskell.TH
import Data.Generics

assertEqual :: (Eq a) => String -> a -> a -> IO ()
assertEqual s a b = when (a /= b) . putStrLn $ "Test " ++ s ++ " failed!"

assertEqual_ :: (Eq a) => a -> a -> IO ()
assertEqual_ = error "assertEqual_ used outside of testCase"

testCase :: String -> Q Exp -> Q [Dec]
testCase name expr = do
    let lowerName = map toLower name
    e' <- [| assertEqual lowerName |]
    pure <$> valD
        (varP (mkName name))
        (normalB (everywhere (mkT (replaceAssertEqual_ e')) <$> expr))
        []
  where
    replaceAssertEqual_ e' (VarE n) | n == 'assertEqual_ = e'
    replaceAssertEqual_ _ e = e

这里的基本思想是生成给定名称的定义,并将assertEqual_引用表达式中每次出现的变量替换为. 感谢 Template Haskell 对Scrap Your Boilerplate的支持,我们不需要遍历整个 AST,只需为每个节点指定一个转换即可。assertEqual lowerNameExp

请注意,它assertEqual_必须是具有正确类型的绑定标识符,因为引用的表达式在传递给之前会进行类型检查testCase。此外,testCase由于 GHC 的阶段限制,必须在与其使用的模块不同的模块中定义。

于 2012-04-05T22:05:28.017 回答
2

现有的答案解释了如何使用元编程来做到这一点,但避免这个问题的一种方法是让匿名测试以他们的名字作为参数。

然后我们可以使用 aData.Map将它们与它们的名称相关联(在这种情况下,我只是使用原始断言,加上map-syntax包中的一些语法糖):

import Data.Map
import Data.Map.Syntax
import Test.HUnit

assertEqual_ x y n = assertEqual n x y

Right tests = runMap $ do
  "test1" ## assertEqual_ 1 2
  "test2" ## assertEqual_ 1 1
  "test3" ## assertEqual_ 3 2

要运行这些,我们可以Data.Map使用以下函数折叠:

  • 将 name 和 assertion-waiting-for-a-name 作为参数
  • 将名称传递给 assertion-waiting-for-a-name
  • 将结果传递AssertionTestCase
  • 运行TestCase
  • 绑定到另一个单子动作,使用>>

我们使用return ()默认的一元动作:

runTests = foldWithKey go (return ()) tests
    where go name test = (runTestTT (TestCase (test name)) >>)

这给出了如下结果:

> go
### Failure:
test1
expected: 1
 but got: 2
Cases: 1  Tried: 1  Errors: 0  Failures: 1
Cases: 1  Tried: 1  Errors: 0  Failures: 0
### Failure:
test3
expected: 3
 but got: 2
Cases: 1  Tried: 1  Errors: 0  Failures: 1
于 2014-10-03T12:25:59.877 回答