單元測試(C#版)

所謂單元測試(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()账磺。

Figure 1. Add unit test project

現(xiàn)在我們的solution就具有了圖2中所示的目錄結(jié)構(gòu),打開剛添加的TestLargest工程下的references痊远,我們可以看到它自動引用了Microsoft.VisualStudio.QuanlityTools.UnitTestFramework垮抗。

Figure 2. Projects in the solution

分別在MyMathLibTestLargest添加代碼如下:

// 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所示)逞姿。

Figure 3. Unit test failed

可以看到辞嗡,我們在單元測試中提供的例子的期望最大值是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所示)达址。

Figure 4. Unit test passed

2. 一個(gè)例子告訴你該如何寫單元測試

我們現(xiàn)在要利用List來寫一個(gè)模擬棧操作的類,該類提供Push趁耗、Pop沉唠、TopEmpty方法,現(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.Emptynull也應(yīng)該是成功的。
(4)連續(xù)地Pop操作嘉冒,確認(rèn)每次取出的都是棧頂元素曹货。
(5)對空棧進(jìn)行PopTop操作咆繁,會拋出異常。

// 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帆竹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子脓规,更是在濱河造成了極大的恐慌栽连,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侨舆,死亡現(xiàn)場離奇詭異秒紧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)挨下,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門熔恢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人臭笆,你說我怎么就攤上這事叙淌。” “怎么了愁铺?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵鹰霍,是天一觀的道長。 經(jīng)常有香客問我茵乱,道長茂洒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任似将,我火速辦了婚禮获黔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘在验。我一直安慰自己玷氏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布腋舌。 她就那樣靜靜地躺著盏触,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赞辩,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天雌芽,我揣著相機(jī)與錄音,去河邊找鬼辨嗽。 笑死世落,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的糟需。 我是一名探鬼主播屉佳,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼洲押!你這毒婦竟也來了武花?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤杈帐,失蹤者是張志新(化名)和其女友劉穎体箕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挑童,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡累铅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炮沐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片争群。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖大年,靈堂內(nèi)的尸體忽然破棺而出换薄,到底是詐尸還是另有隱情,我是刑警寧澤翔试,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布轻要,位于F島的核電站,受9級特大地震影響垦缅,放射性物質(zhì)發(fā)生泄漏冲泥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一壁涎、第九天 我趴在偏房一處隱蔽的房頂上張望凡恍。 院中可真熱鬧,春花似錦怔球、人聲如沸嚼酝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闽巩。三九已至钧舌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涎跨,已是汗流浹背洼冻。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隅很,地道東北人撞牢。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像外构,于是被迫代替她去往敵國和親普泡。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理审编,服務(wù)發(fā)現(xiàn)译暂,斷路器驯绎,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • Android單元測試介紹 處于高速迭代開發(fā)中的Android項(xiàng)目往往需要除黑盒測試外更加可靠的質(zhì)量保障,這正是單...
    東經(jīng)315度閱讀 3,112評論 6 37
  • 什么是單元測試 在計(jì)算機(jī)編程中淑蔚,單元測試(Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設(shè)計(jì)的最...
    HelloCsl閱讀 10,955評論 1 46
  • @Author:彭海波 前言 單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設(shè)計(jì)的最小...
    海波筆記閱讀 4,961評論 0 52
  • Mock 方法是單元測試中常見的一種技術(shù)件炉,它的主要作用是模擬一些在應(yīng)用中不容易構(gòu)造或者比較復(fù)雜的對象勘究,從而把測試與...
    熊熊要更努力閱讀 28,356評論 2 25