編寫基于Property-based的單元測試
作為一個開發者,你可能認為你的職責就是編寫代碼從而完成需求。我不敢茍同,開發者的工作是通過軟件來解決現實需求,編寫代碼只是軟件開發的其中一個方面,編寫可靠的軟件和產出有價值的代碼更加重要。而TDD則是前輩通過經驗總結出的一套切實可行的軟件開發實踐,TDD旨在幫助開發者編寫高質量的代碼。
TDD的過程可以總結為以下幾個步驟:
- 先添加一個測試用例
- 執行測試,查看這個測試的失敗結果
- 對代碼做少量修改
- 再次執行測試,查看測試結果
- 對代碼進行重構,執行測試
單元測試的局限性
設想你要編寫一個加法
功能,接受兩個數字,返回這兩個數字的和。讓我們來按照TDD的流程走一遍:
1.添加一個測試用例
[Fact]
public void Given3And1ShouldReturn4()
{var result = Add(3, 1);result.Should().Be(4);
}
2.執行代碼,發現測試并不能通過,因為我們還沒有實現add方法
3.對代碼做少量修改,讓測試通過
public int Add(int a, int b)
{if(a==3 && b ==1){return 4;}return 0;
}
4.繼續編寫測試
[Fact]
public void Given1And2ShouldReturn3()
{var result = Add(1, 2);result.Should().Be(3);
}
5.修改代碼讓測試通過
public int Add(int a, int b)
{if(a==3 && b ==1){return 4;}if (a == 1 && b == 2){return 3;}return 0;
}
至此為止,你一直在遵守TDD的步驟,測試全部變成了綠色,但是你始終沒有得到正確的Add實現。
哪里出了問題?你也許會覺得,咱們實現的Add方法有問題,我們故意犯了一些顯而易見的錯誤從而給TDD挑毛病。但是我任然可以反駁,他之所以看起來是顯而易見的錯誤是因為對兩個數字求和這樣的需求是每個人都明白的道理,所以你才覺得顯而易見,試想這是一個正式的場景,你也許真的就編寫了這樣的代碼從而讓兩個測試用例都能恰好通過。
如果說我們并不是故意編寫了這樣的代碼,那么單元測試和TDD這種實踐本身可能就有一些瑕疵。
換個角度來說,我們之所以沒有編寫出完整的業務邏輯,是因為單元測試是用例驅動的,而有限的測試用例漏掉了很多可能性。
如果我們對a和b分別取100個隨機值,Add方法都能夠通過,那么我們幾乎很難編寫出上面的Add實現。
[Fact]
public void WhenAddTwoNumberShouldGetSum()
{for (int i = 0; i < 100; i++){var a = GetRandomNumber();var b = GetRandomNumber();var result = Add(a, b);result.Should().Be(a + b);}
}
要想保證這樣的測試通過,你只能編寫出正確的Add實現:
public int Add(int a, int b)
{return a + b;
}
這個測試看起來不錯,通過產生大量隨機的輸入來驅動代碼實現,但是這個代碼存在一個致命的問題,測試代碼和被測試代碼使用了相同的業務邏輯。
//我們期望的數字是a + b
result.Should().Be(a + b);//而被測對象也是a + b
public int Add(int a, int b)
{return a + b;
}
如果a + b這個邏輯本身就有問題,但是因為你在測試代碼里重復了這一有問題的邏輯,實際上你的測試并沒有發現任何問題。
Property-based測試
如果你不在測試代碼里重復a + b這個邏輯,你如何通過這100個隨機輸入來斷言測試的準確性?什么樣的斷言能被用在這100個隨機輸入的測試用例中?
答案是斷言Add這一能力的屬性,某種能夠適用于所有測試用例的屬性。
舉個例子:a + b = b + a
[Fact]
public void A_Add_B_Should_EqualTo_B_Add_A()
{for (int i = 0; i < 100; i++){var a = GetRandomNumber();var b = GetRandomNumber();var result1 = Add(a, b);var result2 = Add(b, a);result1.Should().Be(result2);}}
這一特性正好是加法交換律,如果只是測試交換律還是不能夠保證Add方法的準確性,因為你可以把Add方法實現為a * b。
我們還可以斷言起結合律,即a + b + c = a + (b + c)
[Fact]
public void A_Add_B_Add_C_Should_EqualTo_B_Add_C_Add_A()
{for (int i = 0; i < 100; i++){var a = GetRandomNumber();var b = GetRandomNumber();var c = GetRandomNumber();var result1 = Add(Add(a, b), c);var result2 = Add(a, Add(b, c));result1.Should().Be(result2);}
}
如何實踐Property-based測試
所以什么是Property-based測試?從上面的分析能夠看出Property-based測試實際上提出了兩個策略來保證測試的有效性:
- 隨機產生輸入值,保證足夠多的測試用例
- 找出并斷言功能具有的普遍適應性的屬性
在.NET領域,FsCheck用來進行Property-based測試,Property-based是從Haskell移植過來的,幾乎所有的主流語言都有其移植版本。
下篇我們將介紹如何通過FsCheck來做Property-based測試。