如何用QuickCheck测试Haskell约束类型的子集?
Haskell QuickCheck 约束类型子集测试方案
首先是你定义的Haskell数据类型:
data Document where DocumentKind1 :: { head :: Head, body :: Body } -> Document DocumentKind2 :: { head :: Head, body :: Body } -> Document data Head where HeadKind1 { head_fields :: Int -- 其他字段... } -> Head HeadKind2 { head_fields :: Int -- 其他字段... } -> Head data Body where BodyKind1 { body_fields :: Int -- 其他字段... } -> Body BodyKind2a { body_fields :: Int -- 其他字段... } -> Body BodyKind2b { body_fields :: Int -- 其他字段... } -> Body
由于Haskell不支持约束数据类型,你通过约定遵循以下规则:
- 若
Document为DocumentKind1,则其Head必须是HeadKind1,Body必须是BodyKind1; - 若
Document为DocumentKind2,则其Head必须是HeadKind2,Body必须是BodyKind2a或BodyKind2b。
你有一个仅处理Kind1类型Document的函数:
parseKind1 :: Parser Char Document
问题解答:可以限制生成符合约束的类型子集,且能为每个子集单独测试
下面是两种常用的实现方式:
方式1:用newtype包装约束子集,编写专属Arbitrary实例
为每个符合约束的Document子集定义newtype包装器,然后为这些包装器实现Arbitrary实例,确保生成的实例严格符合规则:
import Test.QuickCheck -- 包装符合DocumentKind1约束的实例 newtype ValidDocumentKind1 = ValidDocumentKind1 Document deriving (Eq, Show) -- 包装符合DocumentKind2约束的实例 newtype ValidDocumentKind2 = ValidDocumentKind2 Document deriving (Eq, Show) -- 为ValidDocumentKind1编写Arbitrary实例 instance Arbitrary ValidDocumentKind1 where arbitrary = do -- 生成符合HeadKind1的Head实例 headVal <- HeadKind1 <$> arbitrary -- 生成符合BodyKind1的Body实例 bodyVal <- BodyKind1 <$> arbitrary return $ ValidDocumentKind1 (DocumentKind1 { head = headVal, body = bodyVal }) -- 可选:定义shrink函数,确保收缩后的实例仍符合约束 shrink (ValidDocumentKind1 doc) = [ ValidDocumentKind1 (DocumentKind1 { head = h', body = b' }) | h' <- shrink (head doc) , b' <- shrink (body doc) ] -- 为ValidDocumentKind2编写Arbitrary实例 instance Arbitrary ValidDocumentKind2 where arbitrary = do headVal <- HeadKind2 <$> arbitrary -- 随机生成BodyKind2a或BodyKind2b bodyVal <- oneof [BodyKind2a <$> arbitrary, BodyKind2b <$> arbitrary] return $ ValidDocumentKind2 (DocumentKind2 { head = headVal, body = bodyVal })
针对ValidDocumentKind1编写测试用例:
-- 假设你有将Document转为字符串的renderDoc函数 prop_parseKind1_correct :: ValidDocumentKind1 -> Bool prop_parseKind1_correct (ValidDocumentKind1 doc) = case parse parseKind1 "" (renderDoc doc) of Success parsed -> parsed == doc Failure _ -> False -- 运行测试 main :: IO () main = quickCheck prop_parseKind1_correct
方式2:直接编写自定义Gen生成器,无需额外类型
如果不想定义新类型,可以直接编写生成符合约束实例的Gen函数,在测试中用forAll指定生成器:
import Test.QuickCheck -- 生成符合DocumentKind1约束的实例 genValidDocumentKind1 :: Gen Document genValidDocumentKind1 = do headVal <- HeadKind1 <$> arbitrary bodyVal <- BodyKind1 <$> arbitrary return $ DocumentKind1 { head = headVal, body = bodyVal } -- 生成符合DocumentKind2约束的实例 genValidDocumentKind2 :: Gen Document genValidDocumentKind2 = do headVal <- HeadKind2 <$> arbitrary bodyVal <- oneof [BodyKind2a <$> arbitrary, BodyKind2b <$> arbitrary] return $ DocumentKind2 { head = headVal, body = bodyVal }
编写测试用例时,用forAll指定使用自定义生成器:
prop_parseKind1_correct :: Property prop_parseKind1_correct = forAll genValidDocumentKind1 $ \doc -> case parse parseKind1 "" (renderDoc doc) of Success parsed -> parsed == doc Failure _ -> False main :: IO () main = quickCheck prop_parseKind1_correct
额外说明
- 如果你的
Head、Body的构造器对应的字段需要更复杂的生成逻辑,可以在arbitrary或自定义Gen中调整,比如生成特定范围的Int、关联字段等。 - 两种方式各有优劣:newtype方式更适合长期维护的测试(类型安全,约束明确);自定义Gen方式更灵活,适合快速编写测试用例。
内容的提问来源于stack exchange,提问作者Delfin
相关产品推荐
相关产品推荐

