初探变异测试

发布时间:

最后更新:

变异测试,跟单元测试、集成测试等等东西一样,都属于软件测试技术。它的核心思想就是随机修改源码,看看这些改动是否能使被现有的测试失败,如果能就说明测试写得不错,反之你的代码可能有问题

最近偶然发现了这玩意,用了下感觉挺不错的,一下子在我的项目里发现了好几个问题。

比如这个(本文里的代码都是 TypeScript):

stryker 的报错 stryker 的报错

javascript
// 对应的测试代码,测试框架 Jest。
it.each([
	"\ta;\tb",
	"a; b;",
	"a; b\t",
	"a\t; b",
])("should split and trim the value %s", value => {
	expect(split(value)).toStrictEqual(["a", "b"]);
});

这段代码的逻辑是把字符串按;分割,去除前后空白并过滤掉空串。在变异测试中,将正则里的一个\s改成了大写的\S后测试仍然通过,这表明它的代码或测试有问题。

仔细一看就会发现,测试中漏掉了分号旁没有空白的情形,如果给测试数据再加一个"a;b"则变异测试不再报错。

当然还有一种做法是改为value.split(";"),因为后面已经用trim去空白了。

传统的单元测试,对这种错误是无能为力的,它的覆盖率一直是 100%。 对此就需要另一种技术,能够对现有的测试进行测试,找出遗漏的用例和多余的代码,这就是变异测试。

基本理论 #

变异测试首次在 1971 年提出,最初是为了定位测试单元的弱点。这个理论是:如果一个变异被引入,同时出现的行为(通常是输出或测试结果)不受影响的话,就说明:变异代码从没有被执行过(产生了无效的代码)或者测试无法定位错误。

这些变异不是瞎改,而是基于良好定义的操作(变异算子),比如:

它不会做一些把关键字const改成cosnt之类的修改,因为语法检查就能够发现它。

有了变异之后,就开始运行现有的测试,这也意味着变异测试只能用于已经拥有测试的项目。在这里每一个变异之后的代码成为一个突变(Mutant),如果它不改变测试的结果就称为存活(Survived);反之称为杀死(Killed)。

那么被杀死的变体越多,就证明代码写得越好。通过配合覆盖率分析,可以计算出突变分数,作为评判测试质量的指标。

变异测试通常运行得很慢,因为一段代码中能替换的地方是非常多的,另外还要通过分析识别出等价的组合。而且对于每个突变,都要找到覆盖到它的测试并运行。

我的小项目有 74 个测试用例,单元测试用时 3.15s,而变异测试用时 3m 45s。

更多示例 #

除了最开始的之外,在我项目里还测试出了更多的问题:

多余的初始化 多余的初始化

这个数组的初始值与构造函数里的重复了,属于无效代码,变异测试通过更改数组中的元素发现了这个问题。

相似函数调换 相似函数调换

这个是测试写得不完善,忘了断言最终结果的行数,导致#开头的行有没有被排除都一样。

无效的情况 #

由于程序本身是复杂的,同时突变也不会深入到第三方代码,所以突变测试即使报告了存活,也并不意味着有错误,必须具体分析。我的项目中也有一些例子:

字符串替换 字符串替换

这是个读取 JSON 文件的函数,因为 JSON.parse 对非字符串类型会调用toString,所以去掉"uft8"传递 Buffer 也正确。

但提前指定编码更好,这样能在读取到缓冲区后就解码,避免创建完整的 Buffer,所以这段代码是正确的,该变异可以忽略。

还有一类误报的情况就是防御性编程,例如下面的:

正则替换 正则替换

这段代码的作用是解析爬虫下载的文件,按行分割并作一些处理。在分割时,使用\n遇到连续的空行会生成空白元素,而\n+则不会。虽然目前抓取的文件没有这种情况,导致变异存活,但保不准未来会遇到,所以这个变异也不用管。

测试框架 #

变异测试虽然小众,但也有很久的年头了,主流语言基本都有工具可用:

这里介绍下本文 stryker-js + Jest 的用法,相当简单只需三步。

首先装依赖:

npm i @stryker-mutator/core @stryker-mutator/jest-runner

然后创建配置文件stryker.conf.js,内容如下

JavaScript
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
// 我的项目是 ESM,如果是 CJS 则用 module.exports =
export default {
	// 包管理器,看你项目用的是啥。
	packageManager: "pnpm",

	plugins: ["@stryker-mutator/jest-runner"],
	testRunner: "jest",
	coverageAnalysis: "perTest",
	
	// 生成 HTML 报告,控制台显示进度。
	reporters: ["html", "progress"],

	// 修改后的临时代码存放路径,测试完自动删除。
	// 注意使用 Jest 的话该路径不能以点开头。
	tempDirName: "stryker-tmp",

	// Jest 如果使用 ESM 的话需要加这个参数。
	testRunnerNodeArgs: ["--experimental-vm-modules"],
};

最后运行命令即可开测

stryker run

成功后的报告保存在 reports/mutation/mutation.html

评论加载中