測試用例代碼覆蓋率的意義
一般的軟件測試關(guān)注測試用例的代碼復(fù)雜度,理想的狀態(tài)是100%的代碼覆蓋率,即測試用例能將所有業(yè)務(wù)代碼執(zhí)行一遍,保證所有的函數(shù)、方法、模塊都被測試過。一些測試工具,如JaCoco等,可以統(tǒng)計測試用例的代碼覆蓋率。
代碼覆蓋率的局限性
舉一個簡單的例子,判斷一個正數(shù)是不是自然數(shù):
package com.example.pitest;
public class NaturalNumber {
private int num;
public NaturalNumber(int num) {
this.num = num;
}
// wrong condition judgement
public boolean isNaturalNumber() {
return num > 0;
}
}
引入JaCoCo gradle插件
plugins {
id 'java'
id 'jacoco'
}
以上函數(shù)isNaturalNumber()對于自然數(shù)的判斷條件明顯錯誤(正確應(yīng)為num >= 0)。然而如果我們的測試用例不夠完善,就會忽略這個錯誤,比如下面的測試用例:
package com.example.pitest;
import org.junit.Assert;
import org.junit.Test;
public class TestNaturalNumber {
@Test
public void PositiveNumberIsNaturalNumber() {
Assert.assertTrue(new NaturalNumber(10).isNaturalNumber());
}
@Test
public void NegativeNumberIsNotNaturalNumber() {
Assert.assertFalse(new NaturalNumber(-10).isNaturalNumber());
}
}
運行JaCoCo test命令
./gradlew jacocoTestReport
以上測試用例使用10和-10作為輸入,測試isNaturalNumber方法能否做出正確判斷。如果我們使用JaCoCo作為代碼測試覆蓋率工具,因為此時num > 0的兩個分支均已覆蓋到,JaCoCo會顯示此時的代碼覆蓋率是100%。但這里的測試用例顯然是不充分的,因為它沒有反映最為關(guān)鍵的num == 0的情況。而一般的代碼覆蓋率工具無法有效地反映出這一點。

mutation test的意義
普通的代碼覆蓋率檢查并不能有效檢測出上述測試用例的缺失,背后原因是以上的兩個測試用例僅僅是分別將num > 0的兩個分支路徑執(zhí)行了一遍,而并沒有真正體現(xiàn)出此判斷條件的作用,如果我們將num > 0 改為 num > 1,上述測試用例一樣可以通過且代碼覆蓋率為100%。
針對類似情況,我們可以引入mutation test框架
Mutation testing (or mutation analysis or program mutation) is used to design new software tests and evaluate the quality of existing software tests. Mutation testing involves modifying a program in small ways. Each mutated version is called a mutant and tests detect and reject mutants by causing the behavior of the original version to differ from the mutant. This is called killing the mutant. Test suites are measured by the percentage of mutants that they kill. ... The purpose is to help the tester develop effective tests or locate weaknesses in the test data used for the program or in sections of the code that are seldom or never accessed during execution.
簡言之,mutation test會在程序編譯或運行時插入微小的差異(mutant),理想的測試用例應(yīng)當能夠檢測出這些差異帶來的程序行為異常。如果一個mutant引發(fā)的程序行為異常能夠被testcases捕捉并導(dǎo)致testcases失敗,則稱mutant被消滅(killed);反之如果mutant帶來的程序行為變化無法被測試用例捕捉,則稱mutant存活(survived)。理想的測試用例應(yīng)當能夠100%消滅mutants,這意味著程序中所有的判斷、運算的作用均已被測試用例運行并捕捉到。
mutation test with PITest
PITest是一個比較流行的mutation test框架,可以在程序編譯時期插入差異,并生成mutation coverage報告,結(jié)合以上例子,如果使用PITest生成測試報告,會發(fā)現(xiàn)測試用例存在覆蓋率不足的問題。
引入PITest gradle插件
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.4.5'
}
}
apply plugin: 'info.solidsoft.pitest'
運行同樣的測試用例
./gradlew pitest
生成的測試報告會顯示,代碼覆蓋度是100%,但mutation覆蓋度僅為67%。
| Name | Line Coverage | Mutation Coverage |
|---|---|---|
| NaturalNumber.java | 100% 4/4 | 67% 2/3 |
我們可以發(fā)現(xiàn),由changed conditional boundary(CONDITIONALS_BOUNDARY)帶來的一個mutation survived。這個mutation的行為是將num > 0改為num >= 0,即更改邊界條件,但測試用例沒能捕捉到這個差異。

為了捕捉到這個mutant,我們需要針對num == 0這種邊界條件添加新的測試用例,而當我們添加了這個測試用例后,錯誤的判斷條件(num > 0)就會被測試出來。