在昨天我們使用了Entity Framework的Codefirst這項技術,它讓我們不用預先建立資料庫,而是可以先定義我們將在程式中使用的資料模型(class),Entity Framework會透過這些模型來產生Database和Table。
我們的Api可以透過資料庫來存取資料之後,要怎麼樣來驗證我們在Repository裡面所定義的查詢條件有沒有錯誤呢?當我們修改Repository的時候又如何來確保沒有改壞程式呢?這時候就是測試該出場的時候了!現在.Net的單元測試函式庫有很多種,但今天我要介紹如何使用Specflow來對DA進行整合測試!
大家可以從Github ApiSample - Tag Day05取得程式碼開始練習喔!
※建立測試資料庫
在撰寫單元測試之前,我們必須先建立我們測試專案的資料庫,測試的資料庫基本上都會和開發時期的時候分開,因為如此一來可以方便我們在撰寫每個測試案例的時候可以得到一個完全獨立的環境,讓測試時所需要面對的資料內容相對單純,也可以減少邊際效應(Side Effect)的發生。
這次我們一樣使用LocalDB來建立測試資料庫,十分簡單又快速!
在LocalDB建立測試專用的資料庫
SqlLocalDB create ApiSample
2. 確認資料庫已成功建立
※建立測試專案
接下來我們就要開始建立我們的測試專案了,一樣建立在我們的DA層之中。
延伸閱讀:
* [.NET][BDD][TDD]BDD with SpecFlow by MS Test (1) – BDD與TDD範例
※什麼是Specflow和SpecRun
Specflow是一套測試的函式庫,我們可以透過它來將測試案例和程式語言做一個結合,讓我們面對測試的時候可以更加得清楚目前測試的情境,更快速的進入狀況。
在使用Specflow時,我們會先將測試案例使用Gherkin來撰寫,它主要包含了一些關鍵字用來描述情境,而Specflow會再將它產生對應的程式碼。
而測試案例中的每一個語句會透過Regular對應到真實的測試程式,還可以自動判斷哪些字是Keyword,轉換為參數丟到函數之中當作測試的Input值。
而Specrun則是一套Specflow的Testrunner (付費軟體,但有提供evaluation版本),它提供了Specflow更好的整合,並且可以產出相當精美的測試報表,執行的速度也相當快速。
延伸閱讀:
* 使用SpecRun產生BDD測試結果報表
※開始撰寫測試
我們昨天已經在Repository寫好了一個查詢Category所包含的Product的方法,現在我們就要針對他來撰寫測試程式。
首先我們希望可以在執行每一個測試案例之前都重置我們的資料庫
新增一個Specflow Feature檔,主要是用來撰寫測試案例
新增一個Specflow Step Definition檔,這邊先來用定義每次都要重建資料庫
在分類商品查詢功能步驟.cs,讓執行每個Scenario之前都刪除資料庫以重建,執行完Scenario都釋放ShopContext
[BeforeScenario]
public void ScenarioSetup()
{
this.shopContext = new ShopContext();
this.shopContext.Database.Delete();
}
[AfterScenario]
public void SecnarioTeardown()
{
this.shopContext.Dispose();
}
在Feature最上面加上,#language: zh-TW,我們就可以用中文來撰寫我們的Testcase,首先描述我們想要測試的項目內容
#language: zh-TW
功能: 分類商品查詢功能
提供給 BL層
查詢分類下的商品有哪些,並自動判斷商品是否在上架時間之內
接下來在Feature檔中描述每個Scenario預設有的資料
背景:
假設 資料庫中有分類資料
| Id | Name |
| 1 | Foods |
| 2 | Drinks |
並且 資料庫中有產品資料
| CategoryId | Name | Price | Cost | ListingStartTime | ListingEndTime | SellingStartTime | SellingEndTime |
| 1 | Hamburger | 99 | 50 | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 |
| 1 | Sandwitch | 89 | 40 | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 |
| 2 | Orange Juice | 40 | 20 | 2013-10-01 | 2014-11-01 | 2013-10-01 | 2014-10-01 |
| 2 | Milk | 35 | 20 | 2013-11-01 | 2014-11-01 | 2013-11-01 | 2014-10-01 |
接下來可以透過Specflow產生測試程式的基本程式碼,點擊滑鼠右鍵
因為我們剛剛已經建立過Step檔了,所以這邊我們選擇複製到剪貼簿
在分類商品查詢功能步驟.cs貼上程式碼
接下來我們只要撰寫將資料填入資料庫的語法就好,這邊值得一提的是Specflow提供了非常棒的輔助功能,它可以直接將文件中的Table,轉換成你指定的格式(Instance Or Set),所以我們在這邊可以直接使用下列語法將上面所列出來的產品資料轉換為IEnumerable<Product>,而日後我們想要增減資料只要在Feature上更動,就可以一併的在測試時套用囉!
var products = table.CreateSet();
實作寫入資料庫的語法
[Given(@"資料庫中有分類資料")]
public void 假設資料庫中有分類資料(Table table)
{
var categories = table.CreateSet<Category>();
foreach (var category in categories)
{
this.shopContext.Categories.Add(category);
}
this.shopContext.SaveChanges();
}
[Given(@"資料庫中有產品資料")]
public void 假設資料庫中有產品資料(Table table)
{
var products = table.CreateSet<Product>();
foreach (var product in products)
{
this.shopContext.Products.Add(product);
}
this.shopContext.SaveChanges();
}
場景: 根據分類序號查詢商品,假設分類下所有商品上架時間都符合,查詢得到所有商品
假設 當查詢分類1的商品時
當 執行分類商品查詢
那麼 得到商品
| Name | Price |
| Hamburger | 99 |
| Sandwitch | 89 |
場景: 根據分類序號查詢商品,假設分類下有商品上架時間還沒到,查詢得到符合的商品
假設 當查詢分類2的商品時
當 執行分類商品查詢
那麼 得到商品
| Name | Price |
| Orange Juice | 40 |
假設 => Arrange
當 => Act
那麼 => Assert
[Given(@"當查詢分類(.*)的商品時")]
public void 假設當查詢分類的商品時(int categoryId)
{
this.queryCategoryId = categoryId;
}
[When(@"執行分類商品查詢")]
public void 當執行分類商品查詢()
{
if (this.productRepository == null)
{
this.productRepository = new ProductRepository(this.shopContext);
}
this.productResult = this.productRepository.GetProductByCategoryId(this.queryCategoryId).ToList();
}
[Then(@"得到商品")]
public void 那麼得到商品(Table table)
{
table.CompareToSet<ProductForCategoryModel>(this.productResult);
}
http://i.imgur.com/diN9oQj.png
延伸閱讀:
* BDD - SpecFlow Introduction
* [30天快速上手TDD][Day 3]動手寫 Unit Test
※本日小結
有了整合測試之後,我們就可以更放心大膽的開發及重構,就好像你在四周都有安全網的地方,不論用多快的速度都不用擔心會衝出去,而測試另一個更大的價值就是透過修正錯誤不斷的補強完善,有了經過妥善維護的測試程式,就不用害怕有人會改壞程式碼,或是有年久失修的程式碼的情況發生了。
關於測試,特別推薦大家參考去年91哥在鐵人賽寫的30天快速上手TDD,相信對測試有興趣的朋友一定會很有幫助,今天的內容若有任何問題歡迎大家一起討論喔^_^
本日文章補充
* 感謝91哥提醒,本篇文章描述的情境並未隔離DB與Repository,實際上應該算是整合測試喔!已修改文章描述
* 其實本篇文章的情境,可能對於大家來說還有點不太容易理解為什麼要這麼做,礙於時間的關係,我會再找時間將這篇文章作補充說明,另外分享一下91哥的相關補充給大家參考^^