Test - 變異(Mutation)測試之你的測試到底是寫爽的,還是有效的?

今天參加了 Agile Community 的變異測試的一個meet up

覺得很有趣 這算是一個寫Unit Test的後續(吧?


變異測試 - 一種改進測試和代碼的 "新" 方法

講師Joseph表示 其實這不是一種新的方式了,這在1970年就有人提出來了 (我都還沒出生呢....) 

變異測試,所謂的變異,就是從英文的 Mutation 翻譯而來的,意義上就是 病變、突異的意思

可以想像我們的程式碼就是身體,如果我們的身體出現了突變、病變的話,就代表我們的身體出了問題了,從程式碼的角度來看就是我們的程式碼有變更了

我們時常寫了許多的測試,但還是很容易出現 Bug 或是有所謂的 「漏網之魚」,或許就可以透過 Mutation Test來找出測試程式碼的漏洞

我自己稍微寫了一點跟講師一樣的程式碼,但是是C#版本的程式碼與測試程式來講解這樣的情況

public class SomeThing
{
    public int Foo(int a, int b)
    {
        var c = 0;
        if (a > 0 && b > 0)
          c = b;
        return c;
    }
}

而我的測試程式碼長這個樣子

[TestClass]
public class TestFoo
{
    [TestMethod]
    public void input_a_is_1_b_is_1_Should_1()
    {
        var someThing = new SomeThing();
        Assert.AreEqual(1, someThing.Foo(1, 1));
    }

    [TestMethod]
    public void input_a_is_1_b_is_n1_should_be_0()
    {
        var someThing = new SomeThing();
        Assert.AreEqual(0, someThing.Foo(1, -1));
    }

    [TestMethod]
    public void input_a_is_n1_b_is_1_should_be_0()
    {
        var someThing = new SomeThing();
        Assert.AreEqual(0, someThing.Foo(-1, 1));
    }

}

我的測試程式碼事實上已經涵蓋100%了,由內建的工具就可以看得到了

我的 Code Coverage 已經是100%了 照理來說我改了任何一個程式碼的內容的話,我的測試程式碼都應該要出現錯誤
但如果我把我的程式碼改成這個樣子

public class SomeThing
{
    public int Foo(int a, int b)
    {
        var c = 0;
        if (a > 0 && b >= 0)
          c = b;
        return c;
    }
}

我把 判斷 b>0 的條件改成 b>=0 我的測試程式碼依然是通過的,代表 2 件事

  • 我寫出來的測試程式碼是無效的

  • Code Coverage 是假的

以測試程式碼無效這件事情來說,這是常有的事,所以我們需要 Mutation Test 來改善我們的程式碼,當然讓程式碼變異的方式有很多種,以下列出其中幾種

  • 條件邊界的變異
  • 反向條件的變異
  • 移除條件的變異
  • 遞增、遞減的變異
  • 常數的變異
  • 返回值的變異
  • 移除程式碼的變異

條件邊界的變異很明顯的,剛才的寫出來的範例已經提到了,嘗試將 ">" 變成 ">=" 或 "<" 變成 "<=" 以這樣的異動來說,整個程式碼的敘述已經變得跟原本不同了,那我們的測試程式碼就應該要抓得出來這樣子的錯誤

反向條件的變異

如果是反向條件的變異,就可以試試將原本的程式碼變為這個樣子

public class SomeThing
{
    public int Foo(int a, int b)
    {
        var c = 0;
        if (!(a > 0 && b >= 0))
          c = b;
        return c;
    }
}

將整個判斷句反向過來,試試看我們的測試程式碼有沒有抓到這樣的錯誤

移除條件的變異

如果是移除條件的變異,就可以試試將判斷的語句移除

public class SomeThing
{
    public int Foo(int a, int b)
    {
        var c = 0;
        c = b;
        return c;
    }
}

將整個判斷語句移除之後,跑跑看 Unit Test 是否通過

遞增、遞減的變異

這個變異是指如果有for迴圈,將其遞增的狀況變更

好比說 原本是 for(int i = 0; i< somethingNumber; i++)

變更為 for(int i = 0; i< somethingNumber; i--)

變更後再跑一次Unit Test是否通過

常數的變異

這個變異是指,假設今天有一個常數已經定義好為 CONST_NUMBER = 5

如今有一段程式碼是 somethingNumber = doSomething(CONST_NUMBER);

將其改成 somethingNumber = doSomething(6);

嘗試看看測試程式碼是否有幫助你找到錯誤

返回值的變異

由範例來修改返回值的程式碼如下,將原本的 return c 變為 return 0

public class SomeThing
{
    public int Foo(int a, int b)
    {
        var c = 0;
        if (a > 0 && b > 0)
          c = b;
        return 0;
    }
}

再試試看這段程式碼被測試程式碼跑過一遍之後會發生什麼事情

移除程式碼的變異

顯而易見的如文字上所說,將程式碼移除,然後再進行測試

public class SomeThing
{
    public int Foo(int a, int b)
    {
        var c = 0;
        return c;
    }
}

跑一下Unit Test 看結果會如何

結語

當然看完上面簡單的範例之後會發現很多程式碼是可以嘗試看看的,如果你的程式碼被移除,或被更動但沒有出錯那就只代表兩件事

  • 這些被移除的程式碼根本不重要,有跟沒有一樣
  • 你的測試程式碼的有效性不足夠,所以要加新的測試

通常是後者就是了XD

不過嘗試變異測試是有前提跟原則的

前提是

這些將要被異變的程式碼最好都有被測試覆蓋

第一點的理由很明顯,如果你的程式碼沒有測試覆蓋,你連原本自己寫的 Code 都不知道是否是正確的,你還做了這些異變,也根本不會知道會不會是對的。

而原則是

  • 異變過後的程式碼要能夠編譯

  • 同一個只使用其中一個變異的方式

  • 確定異變程式碼過後,執行測試應該要是失敗的

第一點,很明顯的,如果你異變過後的程式碼沒辦法編譯,那你連跑 Unit Test 都沒辦法跑

第二點,一般說雖然同時更改很多部分的程式碼,有可能會比較貼近實際上的異動,但只選擇一種變異的方法就已經足以讓自己的測試程式碼更加完善了,當然你可以不只做一種異變測試,你可以做很多個,但建議不要「同時」一次做很多個

第三點,這是一種倒過來思考的方式,有在用TDD的人會想著要如何最小步的讓測試通過,而倒過來就是想著程式碼該如何用最小步的方式讓測試失敗,而語句上要是與原本不同的,大部分的方法已經在上面講了大概了~

現場很少人聽過"變異測試",但為什麼還是這麼多人不知道變異測試呢?

我想應該是 Unit Test 還沒有很普及吧,但變異測試對自己的寫出來的程式碼是確實很有幫助的,如果我有需要維護的案子,且有一定的Code Coverage 那我將會嘗試看看這樣子的方式來試試看我的測試程式碼是否寫的足夠好

當然,這麼廣大的程式碼要手動異動實在是難上加難

所以 wikipedia 上就有介紹很多種不同語言的 Mutation Test 的工具可以使用,至於要不要收費我就不知道了(學講師XD

PS: 工具找出來的變異測試失敗,不一定是真的失敗,有些情境上是工具無法辨別的,不要被工具限制住了

感謝各位大大收看 如有錯誤 請告訴我<(_ _)>