Sunday, February 10, 2019

Mutation Test 简介

Unit test的Line coverage很容易达到100%,但是并不能衡量测试的质量。Mutation test的引入就是解决这个问题。它通过修改程序然后跑现有的tests,如果tests没有fail的话,表示mutation coverage不好,所以mutation test是反着(每次否定一个语句)来跑测试的。Java的mutation test的框架主要是Pitest。

概念

Mutant: 每一个代码改动
Mutation: 修改了的程序
Killed: 一个mutation不能通过unit test,这叫此mutation被杀死了,这是我们想见到的。
Survived: 一个mutation还能通过unit test,这叫此mutation存活了。

下面这个程序是判断一个字符串是否Palindrome。用首尾字符比较然后向中心字符串递归。
public boolean isPalindrome(String inputString) {
    if (inputString.length() == 0) {
        return true;
    } else {
        char firstChar = inputString.charAt(0);
        char lastChar = inputString.charAt(inputString.length() - 1);
        String mid = inputString.substring(1, inputString.length() - 1);
        return (firstChar == lastChar)
                     && isPalindrome(mid);
    }
}

Unit test如下,它的code coverage是100%,但是它mutation coverage只有6/8。它两个地方没有被NEGATE_CONDITIONALS_MUTATOR杀死。

@Test
public void whenPalindrom_thenAccept() {
    Palindrome palindromeTester = new Palindrome();
    assertTrue(palindromeTester.isPalindrome("noon"));
}

第一处,若程序修改成  if (inputString.length() != 0),单元测试仍可以通过,因为任何非空字符串包括noon直接返回true了。这表示我们缺少failure test,加入下面这个:
@Test
public void whenNotPalindrom_thanReject() {
    Palindrome palindromeTester = new Palindrome();
    assertFalse(palindromeTester.isPalindrome("box"));
}

加入了第二个测试后。第二处,也就是程序改成&& !isPalindrome(mid),仍然survived,比较tricky。oo -->false,  noon->true & !f(oo)= true, 双重否定后仍然是成立。所以加入只有有一组首尾相等的测试如neon即可。
@Test
public void whenNearPalindrom_thanReject() {
    Palindrome palindromeTester = new Palindrome();
    assertFalse(palindromeTester.isPalindrome("neon"));
}

Mutator

包括7个默认的Mutator
Conditionals Boundary Mutator。 如<= to < 
Increments Mutator。如i++ to i--
Invert Negatives Mutator。如return i to return -i
Math Mutator。如b+c to b-c
Negate Conditionals Mutator。如== to !=,这个是最普遍的会survive的。
Return Values Mutator。如return new Object() to return null
Void Method Calls Mutator。如someMethod() to (method removed)。这个也普遍。

其他Mutator
Constructor Call Mutator。如Object o = new Object to Object o = null
Empty returns Mutator。如return "abc" to return ""


Config

--threads表示用多少个线程跑
--mutators用什么mutator
--coverageThreshold测试覆盖率
--targetClasses只测试某个包下面的class
--targetTests只测试某个TestClass
--timeoutConst某个测试的时间上限。这个影响全部测试的时间,因为有些mutation可能会导致死循环,当然也算是failed的。

Incremental Analysis

可以配置来减少每次跑测试的时间。因为通过比较代码改动和上次测试结果,新一轮测试只会incremental跑。


Ref

http://pitest.org/quickstart/mutators/
https://www.baeldung.com/java-mutation-testing-with-pitest
https://www.testwo.com/article/869

1 comment: