本篇介紹Entity Framework 實體框架的文章已經到了第十篇了,對實體框架的各個分層以及基類的封裝管理,已經臻於完善,為了方便對基類接口的正確性校驗,以及方便對以后完善或擴展接口進行回歸測試,那么建立單元測試就有很大的必要,本篇主要介紹如何利用VS創建內置的單元測試項目進行實體框架的基類接口測試。
在采用單元測試這個事情上,很多人可能想到了NUnit單元測試工具和NMock工具進行處理,其實微軟VS里面也已經為我們提供了類似的單元測試工具了,可以不需要使用這個第三方的單元測試工具,經試用VS的單元測試工具還是整合性很好,使用非常方便的。
1、實體框架架構及基礎類庫接口
在上次的隨筆《Entity Framework 實體框架的形成之旅--數據傳輸模型DTO和實體模型Entity的分離與聯合》里面,我根據實體框架中混合模式的框架結構,所涉及的架構圖形如下所示。

我們從上圖可以看到,整個框架從下往上分為了幾個明顯的層次,一個數據訪問層DAL層,一個是業務邏輯層BLL層,一個是Facade門面層,各個層的功能不同,這幾個層中以DAL層最為復雜一些,涉及到底層多種數據庫的抽象實現,由於Entity Framework 實體框架本身就是對多種數據庫的實現抽象,因此本文重點針對這個DAL層進行單元測試。
其中的實體框架的公用類庫(WHC.Framework.EF),里面涉及到的IBaseDAL就是數據訪問層的基類接口,具體數據訪問的抽象實現就在BaseDAL的基類上。

在IBaseDAL接口里面,定義了很多我們數據訪問類需要使用的增刪改查、分頁、統計、輔助方法等接口,以及各個方法的異步方法接口,如下所示。
namespace WHC.Framework.EF { /// <summary> /// 數據訪問層基類接口 /// </summary> /// <typeparam name="T">實體對象類型</typeparam> public interface IBaseDAL<T> where T : class { #region 對象添加、修改、刪除 /// <summary> /// 插入指定對象到數據庫中 /// </summary> /// <param name="t">指定的對象</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns> bool Insert(T t); /// <summary> /// 插入指定對象到數據庫中(異步) /// </summary> /// <param name="t">指定的對象</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns> Task<bool> InsertAsync(T t); /// <summary> /// 插入指定對象集合到數據庫中 /// </summary> /// <param name="list">指定的對象集合</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns> bool InsertRange(IEnumerable<T> list); /// <summary> /// 插入指定對象集合到數據庫中(異步) /// </summary> /// <param name="list">指定的對象集合</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns> Task<bool> InsertRangeAsync(IEnumerable<T> list); /// <summary> /// 更新對象屬性到數據庫中 /// </summary> /// <param name="t">指定的對象</param> /// <param name="key">主鍵的值</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns> bool Update(T t, object key); /// <summary> /// 更新對象屬性到數據庫中(異步) /// </summary> /// <param name="t">指定的對象</param> /// <param name="key">主鍵的值</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns> Task<bool> UpdateAsync(T t, object key); /// <summary> /// 根據指定對象的ID,從數據庫中刪除指定對象 /// </summary> /// <param name="id">對象的ID</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> bool Delete(object id); /// <summary> /// 根據指定對象的ID,從數據庫中刪除指定對象(異步) /// </summary> /// <param name="id">對象的ID</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> Task<bool> DeleteAsync(object id); /// <summary> /// 從數據庫中刪除指定對象 /// </summary> /// <param name="t">指定對象</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> bool Delete(T t); /// <summary> /// 從數據庫中刪除指定對象(異步) /// </summary> /// <param name="t">指定對象</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> Task<bool> DeleteAsync(T t); /// <summary> /// 根據指定條件,從數據庫中刪除指定對象 /// </summary> /// <param name="match">條件表達式</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> bool DeleteByExpression(Expression<Func<T, bool>> match); /// <summary> /// 根據指定條件,從數據庫中刪除指定對象(異步) /// </summary> /// <param name="match">條件表達式</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> Task<bool> DeleteByExpressionAsync(Expression<Func<T, bool>> match); /// <summary> /// 根據指定條件,從數據庫中刪除指定對象 /// </summary> /// <param name="condition">刪除記錄的條件語句</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> bool DeleteByCondition(string condition); /// <summary> /// 根據指定條件,從數據庫中刪除指定對象(異步) /// </summary> /// <param name="condition">刪除記錄的條件語句</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> Task<bool> DeleteByConditionAsync(string condition); #endregion
或者一些其他的分頁等復雜的實現接口。
#region 返回集合的接口 /// <summary> /// 返回可查詢的記錄源 /// </summary> /// <returns></returns> IQueryable<T> GetQueryable(); /// <summary> /// 根據條件表達式返回可查詢的記錄源 /// </summary> /// <param name="match">查詢條件</param> /// <param name="sortPropertyName">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns></returns> IQueryable<T> GetQueryable(Expression<Func<T, bool>> match, string sortPropertyName, bool isDescending = true); /// <summary> /// 根據條件表達式返回可查詢的記錄源 /// </summary> /// <param name="match">查詢條件</param> /// <param name="orderByProperty">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns></returns> IQueryable<T> GetQueryable<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true); /// <summary> /// 返回數據庫所有的對象集合 /// </summary> /// <returns></returns> IList<T> GetAll(); /// <summary> /// 返回數據庫所有的對象集合(異步) /// </summary> /// <returns></returns> Task<IList<T>> GetAllAsync(); /// <summary> /// 返回數據庫所有的對象集合 /// </summary> /// <param name="orderByProperty">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns></returns> IList<T> GetAll<TKey>(Expression<Func<T, TKey>> orderByProperty, bool isDescending = true); /// <summary> /// 返回數據庫所有的對象集合(異步) /// </summary> /// <param name="orderByProperty">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns></returns> Task<IList<T>> GetAllAsync<TKey>(Expression<Func<T, TKey>> orderByProperty, bool isDescending = true); /// <summary> /// 返回數據庫所有的對象集合(用於分頁數據顯示) /// </summary> /// <param name="match">條件表達式</param> /// <param name="info">分頁實體</param> /// <returns>指定對象的集合</returns> IList<T> GetAllWithPager(PagerInfo info); /// <summary> /// 返回數據庫所有的對象集合(用於分頁數據顯示,異步) /// </summary> /// <param name="info">分頁實體</param> /// <returns>指定對象的集合</returns> Task<IList<T>> GetAllWithPagerAsync(PagerInfo info); /// <summary> /// 根據條件查詢數據庫,並返回對象集合 /// </summary> /// <param name="match">條件表達式</param> /// <returns></returns> IList<T> Find(Expression<Func<T, bool>> match); /// <summary> /// 根據條件查詢數據庫,並返回對象集合(異步) /// </summary> /// <param name="match">條件表達式</param> /// <returns></returns> Task<IList<T>> FindAsync(Expression<Func<T, bool>> match); /// <summary> /// 根據條件查詢數據庫,並返回對象集合 /// </summary> /// <param name="match">條件表達式</param> /// <param name="orderByProperty">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns></returns> IList<T> Find<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true); /// <summary> /// 根據條件查詢數據庫,並返回對象集合(異步) /// </summary> /// <param name="match">條件表達式</param> /// <param name="orderByProperty">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns></returns> Task<IList<T>> FindAsync<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true); /// <summary> /// 根據條件查詢數據庫,並返回對象集合(用於分頁數據顯示) /// </summary> /// <param name="match">條件表達式</param> /// <param name="info">分頁實體</param> /// <returns>指定對象的集合</returns> IList<T> FindWithPager(Expression<Func<T, bool>> match, PagerInfo info); /// <summary> /// 根據條件查詢數據庫,並返回對象集合(用於分頁數據顯示,異步) /// </summary> /// <param name="match">條件表達式</param> /// <param name="info">分頁實體</param> /// <returns>指定對象的集合</returns> Task<IList<T>> FindWithPagerAsync(Expression<Func<T, bool>> match, PagerInfo info); /// <summary> /// 根據條件查詢數據庫,並返回對象集合(用於分頁數據顯示) /// </summary> /// <param name="match">條件表達式</param> /// <param name="info">分頁實體</param> /// <param name="orderByProperty">排序表達式</param> /// <param name="isDescending">如果為true則為降序,否則為升序</param> /// <returns>指定對象的集合</returns> IList<T> FindWithPager<TKey>(Expression<Func<T, bool>> match, PagerInfo info, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true); #endregion
以及更多的方法接口,我們為了校驗沒有接口都能夠正常工作,就需要對它們進行單元測試。
2、在VS里面創建單元測試項目及編寫單元測試代碼
在VS里面創建內置的單元測試項目如下所示,在添加新項目里面選擇測試->單元測試項目即可,如下圖所示。

為了方便對基類測試,我們還是需要創建一個簡單的代表性數據庫用來檢查基礎的接口操作。
由於前面的系列,已經介紹過了,我們在構建數據訪問層的時候,使用的是基於IOC的方式構建一個對象的接口對象,如這樣代碼IFactory.Instance<IUserDAL>()所示。
而單元測試,基本原理就是我們調用接口,並獲取對應的輸出結果,和我們預期的值進行對比,如果吻合就是正常通過的測試用例。
為了進行基礎類庫的單元測試,我們需要根據實體框架的結構搭建一個具體表的對象項目工程,這個采用代碼生成工具Database2Sharp進行生成就可以了,生成的處理操作如下所示。

這樣根據表快速生成的整個實體框架,就是我們所需要的實體框架項目,具體效果如下所示。

例如我們創建一個查找記錄的單元測試方法代碼如下所示。
namespace TestFrameworkEF { [TestClass] public class TestBaseDAL { private string userId = Guid.NewGuid().ToString(); [TestInitialize] public void Init() { User user = new User() { ID = userId, Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); } [TestCleanup] public void Cleanup() { bool result = IFactory.Instance<IUserDAL>().Delete(userId); Assert.AreEqual(result, true); } [TestMethod] public void FindByID() { User user = IFactory.Instance<IUserDAL>().FindByID(userId); Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); }
其中上面紅色代碼部分就是單元測試的各種標識,包括單元測試類標識,以及初始化、退出清除、測試用例的標識。
上面案例,我們是在單元測試前,在數據庫寫入一條記錄,然后在進行各種單元測試用例的運行及校驗,最后退出的時候,清除我們寫入的記錄。
而記錄的更新和刪除接口,我們具體的單元測試代碼如下所示。
[TestMethod] public void Update() { string newAccount = "Test"; User user = IFactory.Instance<IUserDAL>().FindByID(userId); user.Account = newAccount; bool result = IFactory.Instance<IUserDAL>().Update(user, user.ID); Assert.AreEqual(result, true); user = IFactory.Instance<IUserDAL>().FindByID(userId); Assert.IsNotNull(user); Assert.AreEqual(user.Account, newAccount); } [TestMethod] public void Delete() { var id = Guid.NewGuid().ToString(); User user = new User() { ID = id, Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); result = IFactory.Instance<IUserDAL>().Delete(id); Assert.AreEqual(result, true); }
最后我們整個單元測試的測試代碼如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using WHC.Framework.EF; using EFCore.IDAL; using EFCore.Entity; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq.Expressions; using System.Linq; using WHC.Pager.Entity; namespace TestFrameworkEF { [TestClass] public class TestBaseDAL { private string userId = Guid.NewGuid().ToString(); [TestInitialize] public void Init() { User user = new User() { ID = userId, Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); } [TestCleanup] public void Cleanup() { bool result = IFactory.Instance<IUserDAL>().Delete(userId); Assert.AreEqual(result, true); } [TestMethod] public void FindByID() { User user = IFactory.Instance<IUserDAL>().FindByID(userId); Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void Insert() { var id = Guid.NewGuid().ToString(); User user = new User() { ID = id , Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); user = IFactory.Instance<IUserDAL>().FindByID(id); Assert.IsNotNull(user); Assert.AreEqual(user.ID, id); result = IFactory.Instance<IUserDAL>().Delete(id); Assert.AreEqual(result, true); } [TestMethod] public void InsertRang() { List<User> list = new List<User>(); for(int i = 0; i<3; i++) { var id = Guid.NewGuid().ToString(); User user = new User() { ID = id, Account = "Nunit" + i.ToString(), Password = "Nunit" }; list.Add(user); } bool result = IFactory.Instance<IUserDAL>().InsertRange(list); Assert.AreEqual(result, true); foreach(User user in list) { result = IFactory.Instance<IUserDAL>().Delete(user.ID); Assert.AreEqual(result, true); } } [TestMethod] public void Update() { string newAccount = "Test"; User user = IFactory.Instance<IUserDAL>().FindByID(userId); user.Account = newAccount; bool result = IFactory.Instance<IUserDAL>().Update(user, user.ID); Assert.AreEqual(result, true); user = IFactory.Instance<IUserDAL>().FindByID(userId); Assert.IsNotNull(user); Assert.AreEqual(user.Account, newAccount); } [TestMethod] public void Delete() { var id = Guid.NewGuid().ToString(); User user = new User() { ID = id, Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); result = IFactory.Instance<IUserDAL>().Delete(id); Assert.AreEqual(result, true); } [TestMethod] public void DeleteByExpression() { var id = Guid.NewGuid().ToString(); User user = new User() { ID = id, Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); Expression<Func<User, bool>> expression = p => p.ID == user.ID && p.Account == user.Account; result = IFactory.Instance<IUserDAL>().DeleteByExpression(expression); Assert.AreEqual(result, true); } [TestMethod] public void DeleteByCondition() { var id = Guid.NewGuid().ToString(); User user = new User() { ID = id, Account = "Nunit", Password = "Nunit" }; bool result = IFactory.Instance<IUserDAL>().Insert(user); Assert.AreEqual(result, true); string condition = string.Format("ID ='{0}' ", id); result = IFactory.Instance<IUserDAL>().DeleteByCondition(condition); Assert.AreEqual(result, true); } [TestMethod] public void FindSingle() { Expression<Func<User, bool>> expression = p => p.ID == userId; User dbUser = IFactory.Instance<IUserDAL>().FindSingle(expression); Assert.IsNotNull(dbUser); Assert.AreEqual(dbUser.ID, userId); } [TestMethod] public void GetQueryable() { User user = IFactory.Instance<IUserDAL>().GetQueryable().Take(1).ToList()[0]; Assert.IsNotNull(user); //Assert.AreEqual(user.ID, userId); } [TestMethod] public void GetQueryableExpression() { Expression<Func<User, bool>> expression = p => p.ID == userId; User user = IFactory.Instance<IUserDAL>().GetQueryable(expression, "Account").Take(1).ToList()[0]; Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void GetQueryableExpression2() { Expression<Func<User, bool>> expression = p => p.ID == userId; User user = IFactory.Instance<IUserDAL>().GetQueryable(expression, s=>s.Account).Take(1).ToList()[0]; Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void GetAll() { User user = IFactory.Instance<IUserDAL>().GetAll().Take(1).ToList()[0]; Assert.IsNotNull(user); } [TestMethod] public void GetAllOrderBy() { User user = IFactory.Instance<IUserDAL>().GetAll(s=>s.Account).Take(1).ToList()[0]; Assert.IsNotNull(user); } [TestMethod] public void GetAllWithPager() { PagerInfo pagerInfo = new PagerInfo(); pagerInfo.PageSize = 30; User user = IFactory.Instance<IUserDAL>().GetAllWithPager(pagerInfo).Take(1).ToList()[0]; Assert.IsNotNull(user); } [TestMethod] public void Find() { Expression<Func<User, bool>> expression = p => p.ID == userId; User user = IFactory.Instance<IUserDAL>().Find(expression).Take(1).ToList()[0]; Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void Find2() { Expression<Func<User, bool>> expression = p => p.ID == userId; User user = IFactory.Instance<IUserDAL>().Find(expression, s=>s.Account).Take(1).ToList()[0]; Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void FindWithPager() { PagerInfo pagerInfo = new PagerInfo(); pagerInfo.PageSize = 30; Expression<Func<User, bool>> expression = p => p.ID == userId; User user = IFactory.Instance<IUserDAL>().FindWithPager(expression, pagerInfo).Take(1).ToList()[0]; Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void FindWithPager2() { PagerInfo pagerInfo = new PagerInfo(); pagerInfo.PageSize = 30; Expression<Func<User, bool>> expression = p => p.ID == userId; User user = IFactory.Instance<IUserDAL>().FindWithPager(expression, pagerInfo, s=>s.Account).Take(1).ToList()[0]; Assert.IsNotNull(user); Assert.AreEqual(user.ID, userId); } [TestMethod] public void GetRecordCount() { int count = IFactory.Instance<IUserDAL>().GetRecordCount(); Assert.AreNotEqual(count, 0); } [TestMethod] public void GetRecordCount2() { Expression<Func<User, bool>> expression = p => p.ID == userId; int count = IFactory.Instance<IUserDAL>().GetRecordCount(expression); Assert.AreNotEqual(count, 0); } [TestMethod] public void IsExistRecord() { bool result = IFactory.Instance<IUserDAL>().IsExistRecord(userId); Assert.AreEqual(result, true); } [TestMethod] public void IsExistRecord2() { Expression<Func<User, bool>> expression = p => p.ID == userId; bool result = IFactory.Instance<IUserDAL>().IsExistRecord(expression); Assert.AreEqual(result, true); } [TestMethod] public void SqlExecute() { string newAccount = "newAccount"; string sql = string.Format("update [User] set Account='{0}' Where ID='{1}' ", newAccount, userId); int count = IFactory.Instance<IUserDAL>().SqlExecute(sql); Assert.AreEqual(count, 1); } [TestMethod] public void SqlValueList() { string sql = string.Format("Select ID From [User] "); ICollection<string> valueString = IFactory.Instance<IUserDAL>().SqlValueList(sql); Assert.IsNotNull(valueString); Assert.IsTrue(valueString.Count > 0); } } }
3、運行單元測試
代碼編譯沒有問題后,我們需要檢驗我們的單元測試代碼的正確性,那么只需要在VS的測試菜單里面,執行下面的操作即可。

最后得到的運行結果如下所示,驗證了我們基類代碼的正確性。

