所謂單元測試(unit testing)坪郭,就是對軟件中的最小單元進(jìn)行檢查和驗(yàn)證赊颠,其一般驗(yàn)證對象是一個(gè)函數(shù)或者一個(gè)類。值得一提的是瑰剃,雖然單元測試是開發(fā)者為了驗(yàn)證一段代碼功能正確性而寫的一段代碼齿诉,但是我們寫一個(gè)單元測試的出發(fā)點(diǎn)并不是針對一段代碼或者一個(gè)方法,而是針對一個(gè)應(yīng)用場景(scenario)晌姚,即在某些條件下某個(gè)特定的函數(shù)的行為粤剧。
0. 單元測試的必要性
單元測試不但會使你的工作完成得更輕松,而且會令你的設(shè)計(jì)變得更好舀凛,甚至大大減少你花在調(diào)試上面的時(shí)間俊扳。
(1)單元測試能讓你確定自己的代碼功能和邏輯的正確性,還可以讓你增加對程序的信心猛遍,并且能夠及早發(fā)現(xiàn)程序中的不足。
(2)在寫好功能模塊之前号坡、之中和之后考慮好單元測試怎么寫懊烤,不僅可以讓你更加清楚你寫的功能模塊的邏輯,還能及早地改進(jìn)一些不當(dāng)?shù)脑O(shè)計(jì)宽堆。
(3)每完成一塊功能模塊就用單元測試進(jìn)行驗(yàn)證修改bug腌紧,比整個(gè)軟件寫完再驗(yàn)證調(diào)試要容易得多。而且有了單元測試畜隶,在整體軟件出問題的時(shí)候壁肋,我們可以直接對懷疑的某模塊在單元測試中進(jìn)行debug号胚,這往往比調(diào)試整個(gè)系統(tǒng)要容易得多。
(4)幫助我們及早地發(fā)現(xiàn)問題浸遗。有的時(shí)候?qū)的修改可能會影響看起來毫不相關(guān)的B猫胁,如果沒有單元測試,A的修改checkin之后可能就會引發(fā)比較嚴(yán)重的問題跛锌。而如果在checkin之前能夠運(yùn)行所有的單元測試的話弃秆,B的單元測試可能就會發(fā)現(xiàn)引入的問題,從而阻止此次不當(dāng)修改的checkin髓帽。
我想菠赚,其實(shí)很多程序員都應(yīng)該知道單元測試重要性的那些大道理,只是要改變它就像要戒掉拖延癥一樣郑藏。明明知道那樣不好并發(fā)誓下一次改進(jìn)衡查,卻一直沒有擺脫掉那些惡習(xí)。拜托必盖,不要從明天或者從下一次開始了峡捡,就從現(xiàn)在開始吧!當(dāng)你真正開始去寫單元測試并堅(jiān)持寫筑悴,你會從中得到好處的们拙,那時(shí)候你才會真正領(lǐng)悟到它的必要性。
1. 開始寫你的第一個(gè)單元測試吧
我們先來用VS2012
中自帶的測試模塊來寫一個(gè)簡單的單元測試吧阁吝。
新建一個(gè)solution
砚婆,并添加工程MyMathLib
,在該工程中添加MyMathLib
類突勇,并書寫一個(gè)靜態(tài)的Largest()
函數(shù)來找出一個(gè)整型列表中的最大值装盯。然后添加一個(gè)TestLargest
工程,如圖1
所示甲馋,Add -> New Project
之后選擇Test -> Unit Test Project
埂奈。新建好test工程之后,你會得到一個(gè)test模板定躏,即一個(gè)帶有[TestClass]
attribute標(biāo)記的類和一個(gè)帶有[TestMethod]
attribute標(biāo)記的空方法public void TestMethod1()
账磺。
現(xiàn)在我們的solution
就具有了圖2
中所示的目錄結(jié)構(gòu),打開剛添加的TestLargest
工程下的references
痊远,我們可以看到它自動引用了Microsoft.VisualStudio.QuanlityTools.UnitTestFramework
垮抗。
分別在MyMathLib
和TestLargest
添加代碼如下:
// MyMathLib.cs
namespace FirstUnitTest.MyMathLib
{
public static class MyMathLib
{
public static int Largest(List<int> list)
{
int maxNum = Int32.MaxValue;
foreach (var num in list)
{
if (num > maxNum) maxNum = num;
}
return maxNum;
}
static void Main(string[] args)
{
}
}
}
// UnitTest1.cs
using FirstUnitTest.MyMathLib;
namespace FirstUnitTest.Test
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var list = new List<int>() { 9, 8, 7 };
Assert.AreEqual(9, MyMathLib.MyMathLib.Largest(list));
}
}
}
寫好之后你會發(fā)現(xiàn)有編譯錯(cuò)誤,cannot resolve MyMathLib.MyMathLib.Largest
碧聪,所以我們在TestLargest
工程里光添加using FirstUnitTest.MyMathLib;
是不夠的冒版,還需要在references
中增加對MyMathLib
工程的引用。這樣在TestMethod1()
上單擊右鍵選擇Run Tests
就可以在Test Explorer
里看到單元測試的運(yùn)行結(jié)果(如圖3
所示)逞姿。
可以看到辞嗡,我們在單元測試中提供的例子的期望最大值是9
捆等,運(yùn)行結(jié)果卻是2147483647
。再看一看我們得Largest
方法续室,原來是在對maxNum
進(jìn)行第一次賦值的時(shí)候不小心把Int32.MinValue
寫成了Int32.MaxValue
栋烤。你看,單元測試就是能夠發(fā)現(xiàn)一些意向不到的錯(cuò)誤猎贴。不要以為這里的bug很低級班缎,類似的情況確實(shí)會在現(xiàn)實(shí)中發(fā)生。
我們把上面的錯(cuò)誤更正后她渴,再次運(yùn)行TestMethod1()
就會得到test passed
的結(jié)果(如圖4
所示)达址。
2. 一個(gè)例子告訴你該如何寫單元測試
我們現(xiàn)在要利用List
來寫一個(gè)模擬棧操作的類,該類提供Push
趁耗、Pop
沉唠、Top
和Empty
方法,現(xiàn)在要對這個(gè)類進(jìn)行單元測試苛败。
首先我們要明確這個(gè)類的主要功能:這里的棧用來存儲數(shù)據(jù)满葛,這些數(shù)據(jù)按照存入時(shí)間有序(為方便描述,不妨將最早進(jìn)入的數(shù)據(jù)位置稱為棧底罢屈,最后進(jìn)來的位置稱為棧頂)嘀韧;當(dāng)需要存入數(shù)據(jù)時(shí),采用Push
操作將該數(shù)據(jù)放在棧頂缠捌;當(dāng)需要從棧中取出數(shù)據(jù)時(shí)锄贷,采用Pop
操作將棧頂?shù)臄?shù)據(jù)取出;當(dāng)需要查看棧頂元素時(shí)曼月,采用Top
操作即可得到棧頂元素值谊却;當(dāng)需要知道棧中是否有數(shù)據(jù)時(shí),采用Empty
查看它是否為空哑芹。
那我們就針對這些功能來想想我們應(yīng)用這個(gè)棧的場景吧炎辨,然后就可以把這些場景寫成單元測試。
(1)往一個(gè)空棧中Push
數(shù)據(jù)聪姿,該操作成功的話棧應(yīng)該不空碴萧,并且棧頂元素就是剛Push
進(jìn)去的那個(gè)數(shù)據(jù)。
(2)連續(xù)地往棧中Push
數(shù)據(jù)咳燕,每次操作后查看棧頂元素都是剛剛放進(jìn)去的那個(gè)數(shù)據(jù)勿决。
(3)往棧中Push
特殊的數(shù)據(jù),我們這里存放的是string
招盲,所以添加string.Empty
和null
也應(yīng)該是成功的。
(4)連續(xù)地Pop
操作嘉冒,確認(rèn)每次取出的都是棧頂元素曹货。
(5)對空棧進(jìn)行Pop
或Top
操作咆繁,會拋出異常。
// StackExercise Class
namespace FirstUnitTest
{
public class StackExercise
{
private List<string> _stack;
public StackExercise()
{
_stack = new List<string>();
}
public void Push(string str)
{
_stack.Add(str);
}
public void Pop()
{
if (Empty())
{
throw new InvalidOperationException("Empty stack cannot pop");
}
_stack.Remove(_stack.Last());
}
public string Top()
{
if (Empty())
{
throw new InvalidOperationException("Empty stack cannot get top");
}
return _stack.Last();
}
public bool Empty()
{
return (!_stack.Any());
}
}
}
// Unit Tests
namespace FirstUnitTest
{
[TestClass]
public class TestStackExercise
{
[TestMethod]
public void Test_SuccessAndNotEmpty_AfterPush()
{
// Arrange
var stack = new StackExercise();
var testElement = "testElement";
// Action
stack.Push(testElement);
// Assert
Assert.IsFalse(stack.Empty());
Assert.AreEqual(testElement, stack.Top());
}
[TestMethod]
public void Test_Success_PushMoreThanOnce()
{
// Arrange
var stack = new StackExercise();
var testElement = "testElement_{0}";
// Action & Assert
for (int i = 0; i < 10; ++i)
{
stack.Push(string.Format(testElement, i));
Assert.AreEqual(string.Format(testElement, i), stack.Top());
}
}
[TestMethod]
public void Test_Success_PushEmptyString()
{
// Arrange
var stack = new StackExercise();
string emptyString = string.Empty;
string nullString = null;
// Action & Assert
stack.Push(emptyString);
Assert.AreEqual(emptyString, stack.Top());
stack.Push(nullString);
Assert.AreEqual(nullString, stack.Top());
}
[TestMethod]
public void Test_Success_PopLastTwoElements()
{
// Arrange
var stack = new StackExercise();
var testElement1 = "test1";
var testElement2 = "test2";
// Action & Assert
stack.Push(testElement1);
stack.Push(testElement2);
Assert.AreEqual(testElement2, stack.Top());
stack.Pop();
Assert.IsFalse(stack.Empty());
Assert.AreEqual(testElement1, stack.Top());
stack.Pop();
Assert.IsTrue(stack.Empty());
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_ThrowException_PopFromEmptyStack()
{
var stack = new StackExercise();
stack.Pop();
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_ThrowException_TopFromEmptyStack()
{
var stack = new StackExercise();
stack.Top();
}
}
}
所以顶籽,究竟該如何寫單元測試呢玩般?《單元測試之道C#版》里總結(jié)得很好:Right-BICEP。
Right礼饱,驗(yàn)證結(jié)果(主要功能和邏輯)是否正確坏为;
B,邊界條件是否正確镊绪;
I匀伏,是否可以檢查反向關(guān)聯(lián);這里所謂反向關(guān)聯(lián)蝴韭,是指用反向邏輯來驗(yàn)證我們的結(jié)果够颠,比如說要驗(yàn)證平方根是否正確時(shí),可以求這個(gè)平方根的平方跟我們的輸入是否一致榄鉴。
C履磨,是否可以采用其他方法來cross-check結(jié)果;cross-check是在單元測試中采用與實(shí)際模塊中不同的方法來實(shí)現(xiàn)同樣的功能作為期望結(jié)果庆尘,去與實(shí)際模塊中得到的結(jié)果做對比剃诅。
E,錯(cuò)誤條件是否可以重現(xiàn)驶忌;
P矛辕,性能方面是否滿足條件。
3. 單元測試中不得不說的知識點(diǎn)
(1)斷言Assertion
要驗(yàn)證代碼的行為是否與期望一致時(shí)位岔,我們需要使用斷言來判斷某個(gè)語句為真或?yàn)榧偃缟福约澳承┙Y(jié)果值與期望值是否相等,如IsTrue()
抒抬、IsFalse()
杨刨、AreEqual()
等。
Assert.AreEqual(expected, actual [, string message]);
其中前兩個(gè)參數(shù)很好理解擦剑,分別為期望值和實(shí)際值妖胀,最后一個(gè)可選參數(shù)是發(fā)生錯(cuò)誤時(shí)報(bào)告的消息。如果不提供的話惠勒,出錯(cuò)后會看到這樣的error message:Assert.AreEqual failed. Expected: xx. Actual: yy.
赚抡。如果你的那個(gè)單元測試函數(shù)中有很多Assert.AreEqual的話,你就不清楚究竟是在哪個(gè)Assertion出錯(cuò)的纠屋,而當(dāng)你對每個(gè)Assertion放上相應(yīng)的message
的話涂臣,出錯(cuò)時(shí)就可以一眼看出具體出錯(cuò)的Assertion。
另外,在用斷言進(jìn)行浮點(diǎn)數(shù)的比較時(shí)還需要提供另外一個(gè)參數(shù)tolerance
赁遗。
有時(shí)候每個(gè)test里我們都需要進(jìn)行一系列相同或者類似的斷言署辉,那么我們可以嘗試編寫自定義的斷言,這樣測試的時(shí)候使用這個(gè)自定義的斷言即可岩四。
(2)test 組成
從上面的例子可以看到哭尝,test project與普通project的區(qū)別就是在class和method上面增加了一個(gè)屬性。在不同的框架下這些屬性還是不一樣的剖煌,比如說我們上面用到的VS里自帶的test框架材鹦,使用的是[TestClass]
和[TestMethod]
,而大家最常用的NUint
框架則使用的是[TestFixture]
和[Test]
耕姊。
另外桶唐,還有幾個(gè)attribute在實(shí)際項(xiàng)目中我們也會經(jīng)常用到,那就是[SetUp]
箩做、[TearDown]
莽红、[TestFixtureSetUp]
和[TestFixtureTearDown]
。它們用來在調(diào)用test之前設(shè)置測試環(huán)境和在test之后釋放資源邦邦。前兩個(gè)是per-method
安吁,即每個(gè)用[Test]
修飾的方法在運(yùn)行前后都會調(diào)用[SetUp]
和[TearDown]
;而后兩個(gè)則是per-class
的燃辖,即用于[TestFixture]
修飾的類的前后鬼店。
(3)對于異常的測試
對于預(yù)期的異常,只要在測試方法上添加[ExpectedException(typeof(YourExpectedExcetion))]
屬性即可黔龟。但是需要注意的是妇智,一旦這個(gè)方法期望的異常拋出了,測試方法中剩余的代碼就會被跳過氏身。
所以NUint
里面還有一種方式來驗(yàn)證異常巍棱,即Assert.Throws<ExpectedException>(() => methodToTest());
,這樣就可以在一個(gè)test method里面驗(yàn)證多個(gè)拋出異常的情況了蛋欣。
(4)使用mock對象
單元測試的目標(biāo)是一次只驗(yàn)證一個(gè)方法或一個(gè)類航徙,但是如果這個(gè)方法依賴一些其他難以操控的東西,比如網(wǎng)絡(luò)陷虎、數(shù)據(jù)庫等到踏。這時(shí)我們就要使用mock對象,使得在運(yùn)行unit test的時(shí)候使用的那些難以操控的東西實(shí)際上是我們mock的對象尚猿,而我們mock的對象則可以按照我們的意愿返回一些值用于測試窝稿。
比如說,我們在某個(gè)函數(shù)中需要利用HttpClient
通過SendAsync
方法從某個(gè)EndPoint獲取數(shù)據(jù)進(jìn)行處理凿掂。但是在local測試的時(shí)候不一定能夠連上那個(gè)EndPoint伴榔,或者不能保證那個(gè)EndPoint會返回什么東西。所以我們可以寫mock一個(gè)ResponseHandler
,這樣我們就可以把mock的返回結(jié)果放進(jìn)httpClient
中傳給需要測試的模塊潮梯,這樣就可以測試該模塊內(nèi)后續(xù)部分的處理了骗灶。
internal class MockResponseHandler : DelegatingHandler
{
public HttpStatusCode StatusCode { get; set; }
public HttpContent Content { get; set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken)
{
return await ReturnRespsonse();
}
private Task<HttpResponseMessage> ReturnRespsonse()
{
var response = new HttpResponseMessage()
{
StatusCode = this.StatusCode,
Content = this.Content
};
return Task.Run(() => response);
}
}
var successHttpClient = new HttpClient(
new MockResponseHandler
{
StatusCode = HttpStatusCode.OK
});
var forbidHttpClient = new HttpClient(
new MockResponseHandler
{
StatusCode = HttpStatusCode.Forbidden,
Content = new StringContent(testError)
});
實(shí)際上惨恭,.NET
中現(xiàn)在很多mock對象的框架供選擇(參見http://www.mockobjects.org )秉馏,很多常用的mock都可以直接使用框架,而不需要自己去寫脱羡。
4. 幫助你更好地進(jìn)行單元測試的工具
NUnit
ReShaper
奈何家里的筆記本下載它們一直失敗萝究,所以這里先給個(gè)鏈接,以后有機(jī)會再介紹一下它們吧(⊙﹏⊙)b
參考文獻(xiàn):
《單元測試之道C#版》
單元測試之道C#版 第一章
單元測試 百度百科
談?wù)剢卧獪y試之(一):為什么要進(jìn)行煩人的單元測試锉罐?
C#中的單元測試
A Unit Testing Walkthrough with Visual Studio Team Test