5

我想对我库中的各种类型进行许多类似的测试。

为了简化事情,假设我有许多矢量类型实现Num类,并且我想生成相同的 QuickCheck 属性检查prop_absNorm x y = abs x + abs y >= abs (x+y),该检查将适用于库中的所有类型。

我使用 TH 生成这样的属性:

$(writeTests
    (\t ->
        [d| prop_absNorm :: $(t) -> $(t) -> Bool
            prop_absNorm x y = abs x + abs y >= abs (x+y)
        |])
 )

我生成测试的函数具有以下签名:

writeTests :: (TypeQ -> Q [Dec]) -> Q [Dec]

此函数通过查找我的向量类的所有实例VectorMath (n::Nat) t(以及同时查找 的实例Numreify ''VectorMath并相应地生成所有 prop 函数。 -ddump-splices显示如下内容:

prop_absNormIntX4 :: Vector 4 Int -> Vector 4 Int -> Bool
prop_absNormIntX4 x y = abs x + abs y >= abs (x+y)
prop_absNormCIntX4 :: Vector 4 CInt -> Vector 4 CInt -> Bool
prop_absNormCIntX4 x y = abs x + abs y >= abs (x+y)
...
prop_absNormFloatX4 :: Vector 4 Float -> Vector 4 Float -> Bool
prop_absNormFloatX4 x y = abs x + abs y >= abs (x+y)
prop_absNormFloatX3 :: Vector 3 Float -> Vector 3 Float -> Bool
prop_absNormFloatX3 x y = abs x + abs y >= abs (x+y)

问题是检查了所有手动编写的属性,但没有检查生成的属性。

注意 1:我在同一个文件中生成了和未生成的属性(即 TH 表达式$(..)与其他道具在同一个文件中)。

注 2:用于创建 prop 函数的类型列表是可变的 - 我想VectorMath稍后添加其他实例,因此它们会自动添加到测试列表中。

我认为问题在于 HTF(可能也使用 TH)解析原始文件,而不是生成代码的文件 - 但我不明白为什么会发生这种情况。

所以我的问题是:如何解决这个问题?如果无法使用 TH 生成的道具,那么是否可以对各种类型进行 QuickCheck 测试(即,将它们替换为prop_absNorm :: Vector 4 a -> Vector 4 a -> Bool)?

另一种选择可能是进一步使用 TH 手动将测试条目添加到 htf_Main,但我还没有弄清楚如何做到这一点;它看起来不像是一个干净的解决方案。

4

3 回答 3

3

如果您事先知道生成的属性测试的名称是什么,那么您总是可以手动定义存根以便 HTF 看到它们,例如:

$(generate prop test for Int)
$(generate prop test for CInt)

prop_p1 = prop_absNormInt
prop_p2 = prop_absNormCInt

HTF 会将测试视为prop_p1prop_p2。您不必在这些存根上放置类型签名。

另一个想法是创建你自己的源预处理器来为你添加这些存根(并给它们更好的名字)。您的源预处理器会自动调用htfpp以完成预处理。

如果你告诉我你的 TH 是如何被调用的,我可以告诉你如何编写预处理器。

更新:

鉴于您的评论,我会考虑执行以下操作:

  1. 编写程序生成测试模块源。
  2. 在您的 cabal 项目中包含该程序及其生成的输出。
  3. 如果用户想要更新测试模块,请告诉他们运行程序。

所以 - 测试用例保持不变,直到程序运行以重新生成测试模块。

拥有一个静态测试模块的好处是您可以准确地知道正在测试什么。

拥有一个重新创建测试模块的程序使您能够在新的 Num 实例可用时轻松更新它。

于 2015-09-25T15:21:43.907 回答
1

好的,我设法解决了这个问题。这个想法是使用 TH 来聚合测试并将它们插入到htfMain. 除了我的问题之外,这包括以下步骤:

  1. 将所有可测试属性转换为IO运行 QuickCheck 测试的操作;
  2. 将所有测试汇总到TestSuite;
  3. 将所有测试套件聚合到一个列表中,并将其放入htfMain.

为了使用第 1 步,我不得不使用 HTF 的半内部函数,称为qcAssertion :: (QCAssertion t) => t -> Assertion. 此功能可用,但不建议外用;它允许很好地运行 QuickCheck 测试,并将它们集成到报告中。

为了继续第 2 步,我使用了 HTF 中的两个函数:makeTestSuitemakeQuickCheckTest. 我还使用locationTH 的函数来提供插入带有测试模板的接头的位置的文件名和行(以获得更好的测试日志)。

第 3 步是一个棘手的步骤:为此,我们需要找到所有生成的测试套件。问题是 TH 不允许浏览模块中的所有函数(包括生成的)。为了克服这个问题,我添加了以下类型类:

class MultitypeTestSuite name where
    multitypeTestSuite :: name -> TestSuite

所以我的函数writeTests生成一个新的数据类型data MTS[prop_name]和该数据类型的实例MultitypeTestSuite。这允许我稍后在 htfMain 中使用另一个拼接函数,该函数将使用该类的实例生成测试套件列表reify

aggregateTests :: ExpQ
aggregateTests = do
    ClassI _ instances <- reify ''MultitypeTestSuite
    liftM ListE . forM instances
          $ \... -> [e| multitypeTestSuite $(...) |]

最后,包括所有生成的测试以及手动编写的测试看起来非常简单:

main :: IO ()
main = htfMain $ htf_importedTests ++ $(aggregateTests)

因此,通过调整函数,$(writeTests)我现在能够生成和测试参数类型不同的属性 - 对于同一类型范围内可用的所有类型。测试结果和日志的包含方式与原始测试相同。

至此,问题就彻底解决了。

于 2015-09-27T08:41:49.770 回答
1

HTF 不使用 TemplateHaskell 来收集测试,这会显着减慢编译时间。相反,HTF 使用一个名为htfpp. 在编译器之前htfpp运行(因此在展开 TemplateHaskell 拼接之前)。这意味着在使用 TemplateHaskell 生成测试时,您不能使用自动测试发现。htfpp

我的建议:无论如何,当您使用 TemplateHaskell 时,只需使用 TemplateHaskell 来收集您生成的测试用例。这个功能没有内置在 HTF 中,但是实现这样的功能并不难。就这个:

-- file TH.hs
{-# LANGUAGE TemplateHaskell #-}
module TH ( genTestSuiteFromQcProps ) where

import Language.Haskell.TH

import Test.Framework
import Test.Framework.Location

genTestSuiteFromQcProps :: String -> [Name] -> Q Exp
genTestSuiteFromQcProps suiteName names =
    [| makeTestSuite $(stringE suiteName) $(listE genTests) |]
    where
      genTests :: [ExpQ]
      genTests =
          map genTest names
      genTest :: Name -> Q Exp
      genTest name =
          [| makeQuickCheckTest $(stringE (show name)) unknownLocation
                 (qcAssertion $(varE name)) |]

该函数genTestSuiteFromQcProps采用要生成的测试套件的名称和名称列表,参考您的 QC 属性。genTestSuiteFromQcProps返回类型的表达式TestSuiteTestSuite是 HTF 用来组织测试的类型之一。(htfpp预处理器也使用TestSuite其输出中的类型。)

以下是您将如何使用genTestSuiteFromQcProps

-- file Main.hs
{-# OPTIONS_GHC -F -pgmF htfpp #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where

import TH
import Test.Framework

import {-@ HTF_TESTS @-} OtherTests

prop_additionCommutative :: Int -> Int -> Bool
prop_additionCommutative x y = (x + y) == (y + x)

prop_reverseReverseIdentity :: [Int] -> Bool
prop_reverseReverseIdentity l = l == reverse (reverse l)

myTestSuite :: TestSuite
myTestSuite =
    $(genTestSuiteFromQcProps
         "MyTestSuite"
         ['prop_additionCommutative
         ,'prop_reverseReverseIdentity])

main :: IO ()
main = htfMain (myTestSuite : htf_importedTests)

对于您的情况,您将传递genTestSuiteFromQcProps使用 TemplateHaskell 生成的 QC 属性的名称。

该示例还表明,您可以将使用 TemplateHaskell 函数生成的测试用例与由htfpp. 为了完整起见,以下是 的内容OtherTests

{-# OPTIONS_GHC -F -pgmF htfpp #-}
module OtherTests ( htf_thisModulesTests) where

import Test.Framework

test_someOtherTest :: IO ()
test_someOtherTest =
    assertEqual 1 1
于 2015-09-27T08:51:13.613 回答