在Unit Test時最讓我煩惱的是測試時的測試資料,為了讓每一次測試是一致的受測資料,且不被外在環境干擾,有試過很多方法,如在開始測試時新增測試資料到資料庫,測試時只用這些資料,結束測試時刪除測試資料,或是先準備好內有測試資料的mdb每次測試時複制mdb,不斷的掛載與卸離mdb,說真的這些方法都很蠢,不好維護且各個測試又容易互相影嚮(本測試寫了一筆資料,可能影嚮了下一個測試的結果),寫到最後我就放棄寫需要連資料庫的任何Unit Test,直到前陣子看到有人使用Entity Framework然後使用Mock ObjectContext,決解了測試時需要使用到資料庫這個外部資源,讓Unit Test更Unit,我又開始在新的專案中寫Unit Test。
一、增加ContextMock
在Entity Framework中建立ContextMock沒有多難,幾個按鍵就可以了。
註:因為使用edmx所產生的Model,都是繼承ObjectContext,而在Entity Framework 4.1中改繼承DbContext,所以我習慣叫它為Context。
1.打開Entity Framework的實體資料模型設計檔(edmx),在空白處按右鍵選 加入程式碼產生項目。
2.選擇線上範本後選擇 ADO.NET Mocking Context Generator
註:圖片中第一個範本ADO.NET Unit Testable Repository Generator也是會產生利於測試的Entity Framework,不過呢,使用這個範本要改變我對Repository的寫法,以及Entity變動時要維謢BaseRepositoryTest到瘋掉,而且我不喜歡正式專案中載入測試用的dll,這是它的官網 ,有興趣可自己玩玩。
這樣你的Entity Framework已經可以離線使用了。
1 | //ContextMock的存取是使用在記憶體中的集合,可以把他想成DataSet,原理在第三章說明 |
2 | IModel1 context = new Model1Mock(); |
3 | context.Blogs.AddObject(new Blog() { Id = 1 }); |
4 | context.Blogs.Where(x=>x.Id==1); |
3.不過呢,這個範本少了些東西,必需做一點點的小調整,才能真的拿到專案中使用。
請打開YourModel.Context.tt檔
在104行中加入
: System.IDisposable
在106行中加入
int SaveChanges();
在131行中插入
public int SaveChanges() { return 0; }
public void Dispose() { }
同如下範例:
104 | <#=Accessibility.ForType(container)#> interface I<#=code.Escape(container)#> : System.IDisposable |
108 | foreach (EntitySet entitySet in container.BaseEntitySets.OfType<EntitySet>()) |
111 | IObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.Escape(entitySet)#> { get ; } |
121 | void WriteMockContextBody( EntityContainer container, CodeGenerationTools code ) |
125 | /// The concrete mock context object that implements the context's interface. |
126 | /// Provide an instance of this mock context class to client logic when testing, |
127 | /// instead of providing a functional context object. |
129 | <#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#>Mock : I<#=code.Escape(container)#> |
131 | public int SaveChanges() { return 0; } |
132 | public void Dispose() { } |
註:tt檔是Text Template Transformation Toolkit (俗稱T4)的範本檔,主要用途是可以用範本來產生Code,Visual Studio 2010 中有T4的套件,如Visual T4 ,可以比較方便編輯tt檔。
再搭配IoC就可以很方便的使用ContextMock做單元測試了,怎麼做呢,請看下一章。
二、使用ContextMock
1.在這個範例中IoC,小弟是使用AutoFac 加上CommonServiceLocator (可參考IoC的中繼器:CommonServiceLocator),當然也可以用別的Ioc套件如Unity。
這二個套件都可以用NuGet下載。
什麼!!你不知道什麼是NuGet,那你真要花點時間去了解,NuGet可是比本篇更實用的東西。
請參考黑大的:還在揮汗徒手安裝程式庫? 試試NuGet吧
2.再來可能要改變一點點寫法,請將所有原本使用的Context,改用介面,請在前面加上I,並使用CommonServiceLocator來產生實例。
01 | public class ShippingService |
03 | public void AddToCart( int custimeId, Product item) |
05 | //使用ServiceLocator來取得實例,一般情況使用正常的Context,在單元測試時使用MockContext |
06 | using (var context = ServiceLocator.Current.GetInstance<IShopModel>()) |
09 | var productPremiums = context.Premiums.Where(x => x.ProductId == item.Id); |
12 | context.Carts.AddObject( new Cart()); |
13 | context.SaveChanges(); |
3.撰寫單元測試
02 | public static void MyTestInitialize() |
05 | IShopModel context = new ShopModelMock(); |
06 | context.Products.AddObject( new Product()); //商品資料 |
07 | context.Premiums.AddObject( new Premium()); //優惠資料 |
09 | //註冊實例,讓相關測試在ServiceLocator.Current.GetInstance時用同一個實例 |
10 | ContainerBuilder builder = new ContainerBuilder(); |
11 | builder.RegisterInstance<IShopModel>(context); |
12 | var provider = new AutofacServiceLocator(builder.Build()); |
13 | ServiceLocator.SetLocatorProvider(() => provider); |
17 | public void ShippingService_AddToCartTest() |
19 | //測試時是使用,在MyClassInitialize所準備的資料,不會真的連資料庫 |
20 | var shippingService = new ShippingService(); |
21 | shippingService.AddToCart(1, new Product()); |
註1:別忘了在正式環境中也要加上IoC的註冊,把上面的範例中的
builder.RegisterInstance<IShopModel>(context);
改成如下就可以了
builder.RegisterType<ShopModel>().As<IShopModel>();
註2:這個範例是在同一個Class下的所有測試,共用同樣的測試資料,如果你每一個單元測試都使用不同的測試資料,可以改在每個單元測試中新增測試資料,且建議改用Unity,因為重新註冊在AutoFac中比較麻煩。
三、ContextMock的原理
目前看到的二種Mock的方式,原理都是一樣,產生的Code都很簡單,都是將原本的Context的Set額外定義到Interface,讓原本的Context與ContextMock都是繼承此Interface,我們在撰寫程式時,都是對Interface操作,在搭配IoC讓不同的時機,使用不同的Class,這一點寫過物件導向的人應該不莫生,這就是物件導向的抽象與繼承的應用嘛,而比較麻煩的是要讓一樣的語法,可以對資料庫或集合操作,這部分難的地方Linq幫我們處理掉了,打開YourModelMock.ObjectSet.cs。
01 | public partial class MockObjectSet<T> : IObjectSet <T> where T : class |
04 | private readonly IList<T> m_container = new List<T>(); |
06 | //將List轉成Queryable讓它可以如同對Entity Framework般的操作 |
07 | public System.Linq.Expressions.Expression Expression |
09 | get { return m_container.AsQueryable<T>().Expression; } |
12 | //將List轉成Queryable讓它可以如同對Entity Framework般的操作 |
13 | public IQueryProvider Provider |
15 | get { return m_container.AsQueryable<T>().Provider; } |
你會發現儲存媒體是List,那為什麼List與Entity Framwork的Linq語法可以共用呢?這裡使用了Linq的Queryable與Expression等相關技術,每當使用Linq對Queryable物件操作時,是將語法(如Where)都會轉成Expression,在GetEnumerator時才會使用QueryProvider去真的執行,而List轉成Queryable後QueryProvider是EnumerableQuery<T>,執行時以Linq To Object的方式操作,而Entity Framework的QueryProvider是ObjectQueryProvider,在執行時會剖析Expression轉成Sql對資料庫做操作(小弟有做過雷同的事,請參考Entity Framework批次Update與Delete),這部分的詳細說明小弟會整理到[下一篇IEnumable與IQueryable有什麼不同]。
可以看出這個技巧,不限定只能用在Entity Framework,也可以用在其他ORM的套件中(如nhibernate),只要它支援Linq,只是其他ORM可能就沒有那麼好命有相關的工具幫忙,像小弟最近的一個專案ORM是使用Entity Framework 4.1的Code First,完全沒有工具一切自己手打。
註:請不要問我,使用傳統的ADO.NET方式可以套用此方法嗎?我很久沒用傳統ADO.NET了,很久沒有花心思研究它了,不過我猜是不行,因為要解析Sql,每家的Sql都有差異,要轉成對物件操作,可能太複雜。
没有评论:
发表评论