HTF 不测试 TH 生成的道具

HTF does not test props generated by TH

我想对我的图书馆中的各种类型做一些类似的测试。

为了简化事情,假设我有许多向量类型实现 Num class,并且我想生成相同的 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]

此函数通过 reify ''VectorMath 查找我的向量 class VectorMath (n::Nat) t 的所有实例(同时查找 Num 的实例)并生成所有 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,但我还没有想出如何做到这一点; 它看起来不像一个干净的解决方案。

如果您事先知道生成的 属性 测试的名称是什么,那么您总是可以手动定义存根以便 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 实例可用时轻松更新它。

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

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

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

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

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

class MultitypeTestSuite name where
    multitypeTestSuite :: name -> TestSuite

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

从 class 的实例中生成测试套件列表
aggregateTests :: ExpQ
aggregateTests = do
    ClassI _ instances <- reify ''MultitypeTestSuite
    liftM ListE . forM instances
          $ \... -> [e| multitypeTestSuite $(...) |]

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

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

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

至此问题完全解决

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 returns 类型 TestSuite 的表达式。 TestSuite 是 HTF 用来组织测试的一种类型。 (htfpp 预处理器 als 在其输出中使用 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