Haskell中测试方法与用例全组合执行及类型兼容问题
当然可以实现这种跨类型的交叉测试!你遇到的报错本质是Haskell的强类型特性在列表上的限制——普通列表里的所有元素必须是同一类型,但你的测试用例一个是[Int]对应Int预期值,另一个是[Char]对应Char预期值,类型不匹配,编译器自然会报错。
下面我给你两种可行的解决方案,分别适合不同的场景:
方案一:使用存在类型(支持任意类型的测试用例)
如果想保留"测试方法列表"和"测试用例列表"的结构,同时支持不同类型的用例,我们可以用Haskell的存在量化类型(需要开启ExistentialQuantification语言扩展)来包装不同类型的测试用例,让它们能放进同一个列表里。
代码示例:
{-# LANGUAGE ExistentialQuantification #-} import Control.Applicative -- 包装测试方法:接收列表返回首元素的函数 newtype TestMethod a = TM { runMethod :: [a] -> a } -- 存在类型的测试用例:不管输入/预期是什么类型,只要能被Show和Eq约束(方便打印和比较) data TestCase = forall a. (Show a, Eq a) => TestCase [a] a -- 运行单个测试组合,返回结果字符串 runSingleTest :: TestMethod a -> TestCase -> String runSingleTest (TM method) (TestCase input expected) = let testPassed = method input == expected testDesc = "Input: " ++ show input ++ ", Expected: " ++ show expected in testDesc ++ " => " ++ show testPassed -- 定义你的测试方法(都是多态的,能处理任意类型的列表) testMethods :: [forall a. TestMethod a] testMethods = [TM head, TM (\(x:_) -> x)] -- 定义测试用例:不同类型的用例都能放进TestCase列表 testCases :: [TestCase] testCases = [TestCase [1,2,3] 1, TestCase ['a','b','c'] 'a'] -- 生成所有方法和用例的组合,运行并返回所有结果 runAllTests :: [TestMethod a] -> [TestCase] -> String runAllTests methods cases = unlines [ runSingleTest m c | m <- methods, c <- cases ] main :: IO () main = putStrLn $ runAllTests testMethods testCases
运行这段代码会输出:
Input: [1,2,3], Expected: 1 => True Input: "abc", Expected: 'a' => True Input: [1,2,3], Expected: 1 => True Input: "abc", Expected: 'a' => True
方案说明:
TestCase是存在类型,它把任意满足Show a和Eq a的类型a的输入和预期值包装起来,这样不同类型的用例就能共存于同一个列表。TestMethod包装了多态的列表首元素函数,每个方法都能处理任意类型的列表,完美匹配不同类型的TestCase。- 用列表推导式自动生成所有方法×用例的组合,避免了手动编写大量重复代码。
方案二:直接生成统一类型的测试任务(无需语言扩展)
如果不想开启语言扩展,我们可以直接把每个测试组合转换成统一类型的计算单元(比如String或IO ()),这样所有测试任务就能放进同一个列表里。
代码示例:
import Control.Applicative -- 定义测试方法,带名称方便输出 testMethods :: [(String, forall a. [a] -> a)] testMethods = [("head1", head), ("head2", \(x:_) -> x)] -- 定义不同类型的测试用例生成逻辑 generateIntTests :: (String, [a] -> a) -> String generateIntTests (name, method) = makeTestResult name method [1,2,3] (1 :: Int) generateCharTests :: (String, [a] -> a) -> String generateCharTests (name, method) = makeTestResult name method ['a','b','c'] 'a' -- 通用的测试结果生成函数 makeTestResult :: (Show a, Eq a) => String -> ([a] -> a) -> [a] -> a -> String makeTestResult name method input expected = let testPassed = method input == expected in "[" ++ name ++ "] Input: " ++ show input ++ ", Expected: " ++ show expected ++ " => " ++ show testPassed -- 生成所有测试结果 allTestResults :: [String] allTestResults = concatMap (\gen -> gen <$> testMethods) [generateIntTests, generateCharTests] main :: IO () main = putStrLn $ unlines allTestResults
这段代码的输出和方案一完全一致,而且不需要任何语言扩展。
方案说明:
- 我们把每个类型的测试用例写成一个生成函数(比如
generateIntTests),它接收测试方法,返回对应的测试结果字符串。 - 用
concatMap把所有生成函数和测试方法组合起来,自动生成所有测试结果,同样避免了重复代码。
为什么原来的代码会报错?
你原来的testData列表里,TD [1,2,3] 1的类型是TestData [Int] Int,而TD ['a','b','c'] 'a'是TestData [Char] Char——这两个类型完全不同,Haskell的列表要求所有元素必须是同一类型,所以编译器会尝试把它们统一成一个类型,于是就出现了No instance for (Num Char)的错误(它试图把Char当成Num类型来匹配Int的约束)。
上面的两种方案都解决了这个类型统一的问题,让不同类型的测试组合能被批量处理。
内容的提问来源于stack exchange,提问作者Petras Purlys
相关产品推荐
相关产品推荐

