應該知道的事
案例一、數值區間 1 2 3 4 5 6 假定給任一整數區間 ex: (1,6] = {2,3,4,5,6} [-2,4) = {-2,-1,0,1,2,3} 透過一個function(x)檢查x是否包含在整數區間內, 並撰寫測試,驗証 function(x)是對的。
解析 如上範例所示, 「(」「)」小括號(parentheses)表示OPEN
(不包含,大於或小於) 「[」「]」中括號(square brackets)表示CLOSE
(包含,大於等於或小於等於) (1,6] , 代表這個區間大於 1 小於等於 6,包含的整數有 2、3、4、5、6 [-2,4), 代表這個區間大於等於-2 小於 4,包含的整數有-2、-1、0、1、2、3
這題比較單純,只需要考慮所有的情況, 並且寫成單元測試即可。
x 落在區間內
x 落在左邊界外
x 落在右邊界外
x 落在左邊界上,左邊界為OPEN
x 落在左邊界上,左邊界為CLOSE
x 落在右邊界上,右邊界為OPEN
x 落在右邊界上,右邊界為CLOSE
有幾種特殊的情境,特別說明一下
假設區間為(0,1),這個區間是不包含任何整數
假設區間為(1,1),這個區間是不包含任何整數,且不包含任何數值
假設區間為[1,1],這個區間恰巧包含 1 個整數,且只包含 1 這個整數
假設”區間”為[2,1],或任何左邊界大右邊界的表示,這不是一個正確的區間,將要作例外處理。
讓我們回歸單元測試, 這裡的重點是一個測試只作一件事 , 只把一個情境釐清,並且在測試的程式碼中明確的表達測試目的
1 2 3 4 5 6 7 8 9 10 11 12 private int leftBound = 1 ;private int rightBound = 6 ;private int testNum = 4 ;[TestMethod ] public void IncludeWhenLeftOpenRightClose (){ var checker = new RangeChecker(Bound.Open,this .leftBound,Bound.Close,this .rightBound); bool expect = false ; bool result = checker.IsContains(testNum); Assert.IsTrue(result); }
案例二、現在時間轉字串 1 2 3 寫一個方法GetNowString,不傳入任何參數, 取得現在的時間字串,需要精準到豪秒。 再寫一個測試去測試這個方法是對的‧
版本 1 最簡單的寫法:
1 2 3 4 5 6 7 public class DateHelper { public string GetNowString () { return DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss ff" ); } }
撰寫測試
1 2 3 4 5 6 7 8 9 [TestMethod ] public void GetNowString (){ var string expect = "2017-04-19 20:45:17.88" ; string result = dater.GetNowString(); Assert.AreEqual(expect, result); }
解析 1 GetNowString
與系統的時間DateTime.Now
, 是具有耦合性,要解耦需要透過一些 IoC 的手段去處理。
版本 2 利用繼承的方法,作出假的類別
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class DateHelper { protected DateTime now; protected virtual DateTime GetNow () { now = DateTime.Now; return now; } public string GetNowString () { GetNow(); return now.ToString("yyyy-MM-dd HH:mm:ss.ff" ); } } class StubDateHelper : DateHelper { protected override DateTime GetNow () { return now; } public void SetNow (DateTime datetime ) { now = datetime; } }
撰寫測試
1 2 3 4 5 6 7 8 9 10 [TestMethod ] public void GetNowString (){ StubDateHelper dateHelper = new StubDateHelper(); var fakeNow = new DateTime(2017 ,4 ,19 ,20 ,45 ,17 ,880 ); dateHelper.SetNow(fakeNow); string expect = "2017-04-19 20:45:17.88" ; string result = dateHelper.GetNowString(); Assert.AreEqual(expect, result); }
解析 2 基本上這樣就可以測試了, 原來的代碼,經過一定的重構, 透過virtual
方法 GetNow, 將Datetime.Now
作了隔離 適當利用假類別,取代掉 GetNow 的方法。
這樣夠好了,但是我們可以看看另一種作法
版本 3 先看看我們的DateHelper
, 在這裡我們將 GetNow 交由 IDateProvider 的類別去實作, 如此一來就斷開了耦合性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class DateHelper { private IDateProvider DateProvider; public DateHelper (IDateProvider dateProvider ) { this .DateProvider = dateProvider; } public string GetNowString () { var now = this .DateProvider.GetNow(); return now.ToString("yyyy-MM-dd HH:mm:ss.ff" ); } }
實作 IDateProvider 的類別, 在這裡其實不重要.
1 2 3 4 5 6 7 public class DateProviderV1 : IDateProvider { public DateTime GetNow () { return DateTime.Now; } }
讓我們看看測試, 在這裡我們透過一個假的IDateProvider
的實作DateProviderStub
, 完成了測試, IDateProvider 將DateTime.Now
作了隔離, 並且提供更容易修改的假物件(僅僅需要實作觀注的方法即可,不用擔心繼承帶來的附作用)
1 2 3 4 5 6 7 8 9 10 [TestMethod ] public void GetNowString (){ DateProviderStub dateProvider = new DateProviderStub(); dateProvider.now = new DateTime(2017 , 4 , 19 , 20 , 45 , 17 , 880 ); var dateHelper = new DateHelper(dateProvider); string expect = "2017-04-19 20:45:17.88" ; string result = dateHelper.GetNowString(); Assert.AreEqual(expect, result); }
1 2 3 4 5 6 7 8 public class DateProviderStub : IDateProvider { public DateTime now; public DateTime GetNow () { return now; } }
圖例解析 我們剛剛究竟幹了什麼? 看看原本的情況,本來的方法因為相依與Datetime
而無法測試 讓我們開始下刀, 先用一個新的方法GetNow
將它與待測的方法作分割, 但是對整個類來說仍舊是耦合。 繼續把這刀往下切, 我們墊一層介面, 待測方法不再直接呼叫GetNow
而是透過介面執行,當然會有額外實作介面與注入的功(請參考程式碼,不在此處繪出了.) 最後,別忘了我們的目的 測試原本的待測方法, 我們可以透過一個假的
類, 來操控他的行為(ex:凍結時間). 如此一來,就可進行測試了. 另外,這種被待測方法呼叫後 會回傳一個假值的方法或類 被叫作STUB
案例三、發送郵件 事先聲明,這題沒有程式碼, 有興趣實作的人可以試試看. 如果可以分享實作後的資訊給我更好 XD
Q:註冊發送郵件如何寫單元測試?
解析 很明顯的發送郵件需要依賴外部的郵件系統, 這裡就會有耦合性,我們可以參考案例 2 的方式解耦 不過發送郵件並不會有回傳值, 我們要如何驗証正確性呢?
A:檢查調用次數、參數
圖例解析 在案例 2 的單元測試, 我們透過 STUB 偽造的回傳值完成測試 並執行驗証. 但是在沒有回傳的值的方法中(被稱作MOCK
) 我們只能透過傳遞的參數(如果有多載) 與方法被調用的次數來進行驗証。
重點摘要
單元測試要能清楚表達測試的目的(達意 )
單元測試一次只作一件事
new 本身就是一種邏輯 一種偶合
static 是一種高偶合
繼承也是高偶合,能使用繼承的情境很少
STUB & MOCK
STUB 會有回傳值,可以在 Unit Test 作驗証(ASSERT)
MOCK 沒有回傳值,可以在 MOCK 本身中 作驗証(ASSERT)
其它
SLIM
注入相依的幾種方式
Pool
Constructor
Property
書單 : XUnit Test Patterns
直播影片 如果連結失效,煩請告知.
文章內容如有謬誤,煩請指正.
(fin)