寫了 Unit Test 然後呢

一般來說談到要讓程式碼的品質變好,多數人想到的除了規範團隊的 Coding Style 、Code Review 之外,就屬撰寫 Unit test 來確保 production code 可以確實符合商業需求。

但是這邊就有一個問題產生了,究竟我要寫多少單元測試才夠呢?我的覆蓋率已經 100% 了,是否代表萬無一失了?但 100% 根本神話,而且這樣要寫超多單元測試,寫測試的時間都可以拿來做下一個需求了。。。

上述的問題在尚未引入 Mutation test 之前看起來是真的挺無解,也容易陷入覆蓋率的迷思之中。

Mutation Test 是什麼

Mutation test 說實話不是個新概念,他的歷史應該可以往回推到 1978 年的這篇論文。他的基本概念就是藉由改變 production code 來檢驗你的單元測試能否抓到這些改變,若能夠抓到就代表你的測試能夠確保目前的程式碼夠強健,能夠確保在商業邏輯改變後你可以知道什麼東西會跟以前不同。反之,若改變 production code 後單元測試卻沒有反應出來,就代表需要再補強單元測試。

會用到的必要 Packages

junit 目前第五版也是可以使用 PIT,不過作法比較不同,需要引用不同的 Packages,可以參考這個來試試。

Demo Code

範例程式

以下這個是一個迴文函式,目的是用來判斷一個傳入的字串是否屬於迴文。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class 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 = "";
            if (inputString.length() > 1){
                mid = inputString.substring(1, inputString.length() - 1);
            }
            return (firstChar == lastChar) && isPalindrome(mid);
        }
    }
}

以下則是這個函式的測試程式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AppTest 
{
    @Test
    public void testApp()
    {
        // arrange
        boolean except = false;
        Palindrome palindrome = new Palindrome();

        // act
        boolean result = palindrome.isPalindrome("Madam");
        // assert
        assertEquals(except, result);
    }
}

假設這是一個 Maven 的專案,那麼你可以執行以下指令來驗證測試是否通過

  • mvn clean
  • mvn compile
  • mvn test

PIT 基本設定

以下的設定都是基於假設目前是一個 Maven 專案來進行。

首先在 dependencies 這個區段中加入以下片段

1
2
3
4
5
<dependency>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.4.9</version>
</dependency>

接下來在 buildpluginManagementplugins 區段中加入以下片段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.4.9</version>
    <executions>
        <execution>
            <id>pit-report</id>
            <phase>test</phase>
            <goals>
                <goal>mutationCoverage</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <targetClasses>
            <param>com.ben.app.Palindrome</param>
        </targetClasses>
        <targetTests>
            <param>com.ben.app.*</param>
        </targetTests>
    </configuration>
</plugin>

這裡需要特別注意的是要指明 targetClassestargetTests 這兩個區塊內的設定是相當重要的,其中 targetClass 表示了 PIT 可以改變的範圍在哪裡,若打上星號的話,會將所有 packages 內的類別全數改一輪。而 targetTests 則是指明了,改變過後要跑得測試是在哪裡,而這也是它判定你的測試是否真有抓出變異的依據。

PIT 提供的變異種類

PIT 提供了非常多的變異可以使用,不過最常也最好用的就是一般預設的方式。而所有 PIT 提供的變異內容可以在這裡看到。

以下將只列出使用預設的運算子。

  • Conditionals Boundary Mutator

    變更判斷式的上下界線。若原本是大於,則改成大於等於,若原本是小於等於,就改成小於。

  • Increments Mutor

    累加變成累減 (i++ 改成 i–)

  • Invert Negatives Mutor

    將返回的數值乘上 -1

  • Math Mutor

    加減乘除改成相對的符號,像是加號變減號

  • Negate Conditionals Mutor

    將判斷式做反轉,相等於改成不等於

  • Return Values Mutator

    將回傳值改變,若為 value type 則會傳回相反的值 (ex. 返回 true 改成 false, 返回 int x 改成 x + 1), 若為 reference type 則返回 null。

  • Void Method Calls

    將 void 函式移除

PIT 報表怎麼看

從範例程式抓下來若直接執行的話,應該可以看到如下的報表結果

mutation report01

若程式碼該行標記成紅色,代表有變異沒有被抓出來,而該行旁邊的 1, 2, 4 則表示該行進行多少次變異。

共有兩種結果會標記成紅色:

  • 測試尚未覆蓋 (no coverage)
  • 變異存活 (survived)

而若將範例程式被註解的測試打開後,在執行一次「編譯、測試、執行變異」再來看一次報表

mutation report02

則邊可以看到原本標記為 no converage 的已經沒有了,而且 mutation coverage 的分數也比上一次來的高,代表整體品質有所上升。至於其他的變異要怎麼消除提高分數,可以讓有興趣的人來嘗試一下。

Chinese Reference

English Reference