IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    初探变异测试

    Kaciras 的博客发表于 2023-02-14 17:29:49
    love 0

    变异测试,跟单元测试、集成测试等等东西一样,都属于软件测试技术。它的核心思想就是随机修改源码,然后运行测试,如果通过则说明你的测试不完善,或者存在多余代码。

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

    比如这个(本文里的代码都是 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 年提出,最初是为了定位测试单元的弱点。这个理论是:如果一个变异被引入,同时出现的行为(通常是输出或测试结果)不受影响的话,就说明:变异代码从没有被执行过(产生了无效的代码)或者测试无法定位错误。

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

    • 把a + b改为a - b。
    • 把a > b改为a < b。
    • 把某条throw new Error()语句删除。
    • 把if语句的条件改为true或false。
    • 把某个函数里的代码直接清空。

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

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

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

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

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

    更多示例 #

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

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

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

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

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

    无效的情况 #

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

    字符串替换字符串替换

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

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

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

    正则替换正则替换

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

    测试框架 #

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

    • 本文用得是 Stryker,它同时有 JavaScript、C#、Scala 三种语言的实现。
    • C++ 也有 Mull。
    • JAVA 语言的话可以用 Pitest。
    • Golang 的比较新 go-mutesting。

    这里介绍下本文 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。



沪ICP备19023445号-2号
友情链接