宣布在 Haskell 中引入变异测试
摘要
变异测试现已在 sydtest Haskell 测试框架中正式发布,开发者可通过自动生成代码变异并验证测试套件是否能捕获这些变异,从而客观评估测试质量。作者的动机源于 AI 生成代码(通过 Claude)的兴起,以及对测试覆盖率进行客观、自动化度量的需求。
<p><a href="https://lobste.rs/s/xwhlul/announcing_mutation_testing_haskell">评论</a></p>
查看缓存全文
缓存时间: 2026/06/05 02:16
# 宣布在 Haskell 中引入变异测试
来源:https://cs-syd.eu/posts/2026-06-03-mutation-testing-in-haskell
变异测试现已在 [sydtest](https://github.com/NorfairKing/sydtest) 中正式发布。这是在 AI 生成代码时代迈向更合理开发工作流的重要一步。
## 什么是变异测试?
> 变异测试旨在通过自动修改代码并验证测试是否开始失败,来提升测试套件的质量。
或者换一种说法:
> 变异测试就像是测试的类型系统,它验证测试是否对代码进行了充分的测试。
### 示例
来看这个简单的函数:
``
canCastFireball :: Int -> Int -> Bool
canCastFireball level mana =
level >= 5
&& mana >= 10
``
以及对应的测试套件:
``
spec :: Spec
spec = do
describe "canCastFireball" $ do
it "allows powerful wizards" $
canCastFireball 10 50 `shouldBe` True
it "rejects exhausted powerful wizards" $
canCastFireball 10 0 `shouldBe` False
it "rejects weak wizards" $
canCastFireball 1 10 `shouldBe` False
``
你觉得这是一个好的测试套件吗?你凭什么这样判断?
我们可以认为,好的测试套件能够捕获更多你犯的错误。
变异测试的做法是模拟这些错误,并检验测试套件是否真的能捕获到它们。
在这个例子中,变异测试可能会生成这样一个变异:
``
canCastFireball :: Int -> Int -> Bool
canCastFireball level mana =
level >= 5
< && mana >= 10
---
> && mana > 10
``
当我们再次运行同一测试套件时,所有测试依然通过。这意味着如果你真的犯了这个错误,测试并不会发现它。这种情况被称为**存活变异**,是*不期望出现*的。
当一个变异存活时,你可以添加测试来**覆盖**它。例如,下面这个测试就可以覆盖它:
``
spec :: Spec
spec = do
describe "canCastFireball" $ do
it "allows barely-energetic wizards" $
canCastFireball 8 10 `shouldBe` True
``
现在在变异代码上运行测试套件时,这个测试将会失败。这意味着如果你真的犯了这个错误,(新的)测试套件就会发现它。这种情况被称为**被杀死的变异**,是*期望出现*的。(先别让我吐槽这套术语有多让人困惑,而且还充满暴力色彩。)
变异测试引擎会自动生成变异并运行(对应的)测试。理想情况下,它会生成大量变异,其中没有任何一个存活。
为了获得最大保障,你应该覆盖每一个变异。但实际上,你也可以选择禁用其中一些。
## 为什么现在开始做变异测试?
我已经使用编程 agent(Claude)一段时间了,逐渐发现自己对它生成的代码越来越缺乏信心。这不一定是因为它不如我聪明(很多时候它确实更聪明),而是因为它在相同时间内能产出的代码量实在太大了。
我已经设置了完善的指令,要求它编写测试、回归测试和属性测试,但它经常完全无视我的指令,或者写出毫无意义的测试。真正能帮到我的,只有一个[不依赖 AI 的 CI 系统](https://nix-ci.com/),在任何检查失败时告知我。
所以我的目标是构建一个检查项:如果某次变更测试不充分,该检查就会失败——而且不依赖任何主观标准来判断"充分"的含义。变异测试让我拥有一个**完全客观**的标准,它**独立于我的项目**,定义在另一个仓库中,这样我的 agent **就无法作弊**。
## 如何上手?
变异测试现在已作为 [Sydtest](https://github.com/NorfairKing/sydtest) 的一部分正式发布。
### Nix 检查
你可以像这样在 `flake.nix` 的 `checks` 中添加变异检查:
``
checks.x86_64-linux.mutation = pkgs.haskellPackages.sydtest.mutationCheck {
name = "my-mutation-check";
packages = [
"my-package"
"my-other-package"
];
};
``
Sydtest 会处理其余的工作,并生成美观的报告——既有人类可读的……
Report
也有机器可读的:
``
{
"outcome": "uncovered",
"mutation": {
"id": ["Money.Amount", "Cmp", "801", "79", "92", "<", "1" ],
"operator": "Cmp",
"original": ">",
"replacement": "<",
"module": "Money.Amount",
"source_file": "src/Money/Amount.hs",
"line": 801,
"end_line": 801,
"col_start": 79,
"col_end": 92,
"context_before": [
"",
"-- | Validate that an 'Amount' is strictly positive. I.e. not 'zero'.",
"validateStrictlyPositive :: Amount -> Validation"
],
"source_lines": [
"validateStrictlyPositive amount = declare \"The Amount is strictly positive\" $ amount > zero"
],
"mutated_lines": [
"validateStrictlyPositive amount = declare \"The Amount is strictly positive\" $ amount < zero"
],
"context_after": [],
"covering_tests": {
"really-safe-money-autodocodec-test": [],
"really-safe-money-test": []
},
"timeout_micros": 30000000
}
}
``
### 禁用变异
有时候你并不关心某段代码是否经过了完整的变异测试。一个典型的例子(在我看来)是调试日志:
``
doAThing = do
logDebug "Doing a thing"
doTheThing
``
删除 `logDebug` 这一行是一个合法的变异,但我根本不在乎是否要测试它。
在这种情况下,我可以添加一个注解:
``
{-# ANN doAThing ("DisableMutationsFor logDebug" :: String) #-}
doAThing = do
logDebug "Doing a thing"
doTheThing
``
还有其他注解可用,支持按模块、按变异或按绑定来禁用变异。
## 总结
Haskell 中的变异测试已经可以上手试用了。我已经在 [NixCI](https://nix-ci.com/) 中使用它,最新版本的 [`really-safe-money`](https://github.com/NorfairKing/really-safe-money/tree/master) 也已经完成了完整的变异测试覆盖。
如果你最终尝试了它,请告诉我。我很乐意跟你深聊这个话题。
相似文章
上下文使测试更具可重用性
作者分享了在Guile中设计测试框架的经验,重点探讨了向测试定义添加上下文如何使测试更可重用并改善开发者体验。
基于Markdown的测试套件
作者解释了为EndBASIC的编译器和虚拟机切换到基于Markdown的测试套件的原因,目的是让这些测试作为LLM学习该语言独特特性的权威文档。
对CAD库进行基线测试
作者描述了如何使用SVG输出和tasty-golden库为Waterfall-CAD Haskell库实现基线/视觉回归测试。
Anthropic 如何借助 Claude Code 进行大规模代码迁移(11 分钟阅读)
Anthropic 概述了使用 Claude Code 进行大规模代码迁移的六步流程,重点包括创建规则手册、依赖关系映射以及通过强大的评审模型进行迭代验证。
Show HN: Hsrs – 用于 Rust 的类型安全 Haskell 绑定生成器
Hsrs 是一个类型安全的 FFI 绑定生成器,允许从 Haskell 调用 Rust 代码,具有自动内存管理、类型转换和 Borsh 序列化功能。它在 Rust 中提供注解,并生成符合语言习惯的 Haskell 包装器。