1. 有關上篇請參見《使用IdleTest進行TDD單元測試驅動開發演練(1)》,有關本篇用到Entity Framework Code First請參見《使用NuGet助您玩轉代碼生成數據————Entity Framework 之 Code First》,而用的我的類庫參照IdleTest。
2. 本文只用了簡單的Entity Framework演練單元測試,着重於Testing,而不是實現,並不會涉及事務、效率等問題。html
3. 回顧上一篇裏面講到的是針對業務層的測試,正如敏捷中厲行的多與用戶溝通,在書《C# 測試驅動開發(Professional Test Driven Development with C#)》中做者就推薦TDD中單元測試的編寫應有業務人員與需求人員參與,不是參與編碼,而是參與單元測試的用例制定,固然了不涉及業務層面的代碼也不須要如此。好比註冊功能有多少種場景均可以在單元測試中體現出來,這時就要針對每種場景編寫至少一個單元測試的方法,其命名也就尤其重要,由於要讓他們看懂每一個方法對應什麼樣的場景。如下就是我改造後的對UserService進行測試的代碼,其中每一個類對應一個功能模塊,類中的每一個方法則對應該功能的每一種場景,這樣以便於與需求以及相關業務人員肯定開發需求後再編碼,減小了開發中的需求變動。數據庫
public abstract class BaseUserServiceTest { protected UserTestHelper UserTestHelper = new UserTestHelper(); private IUserRepository userRepository; protected IList<UserModel> ExistedUsers; protected abstract IUserService UserService { get; } /// <summary> /// IUserRepository模擬對象 /// </summary> public virtual IUserRepository UserRepository { get { if (this.userRepository == null) { StubIUserRepository stubUserRepository = new StubIUserRepository(); //模擬Get方法 stubUserRepository.GetExpressionOfFuncOfUserModelBooleanFuncOfIQueryableOfUserModelIOrderedQueryableOfUserModelString = (x, y, z) => { return this.ExistedUsers.Where<UserModel>(x.Compile()); }; //模擬GetSingle方法 stubUserRepository.GetSingleString = p => this.ExistedUsers.FirstOrDefault<UserModel>(o => o.LoginName == p); //模擬Insert方法 stubUserRepository.InsertUserModel = (p) => this.ExistedUsers.Add(p); this.userRepository = stubUserRepository; } return this.userRepository; } } [TestInitialize] public void InitUserList() { //每次測試前都初始化 this.ExistedUsers = new List<UserModel> { UserTestHelper.ExistedUser }; } #region Login Test [TestCategory("登錄場景")] public virtual void 當用戶信息所有爲空或帳戶爲空或密碼爲空或帳戶錯誤或密碼錯誤或帳戶密碼均錯誤都登錄失敗() { //驗證登錄失敗的場景 AssertCommon.AssertBoolean<UserModel>( new UserModel[] { null, new UserModel(), new UserModel { LoginName = string.Empty, Password = UserTestHelper.ExistedPassword }, //帳戶爲空 new UserModel { LoginName = UserTestHelper.ExistedLoginName, Password = string.Empty }, //密碼爲空 new UserModel { LoginName = UserTestHelper.ExistedLoginName, Password = UserTestHelper.NotExistedPassword }, //密碼錯誤 new UserModel { LoginName = UserTestHelper.NotExistedLoginName, Password = UserTestHelper.NotExistedPassword }, //帳戶密碼錯誤 new UserModel { LoginName = UserTestHelper.NotExistedLoginName, Password = UserTestHelper.ExistedLoginName } //帳戶錯誤 }, false, p => UserService.Login(p)); } [TestCategory("登錄場景")] public virtual void 當帳戶密碼所有正確時登錄成功() { //帳戶密碼正確,驗證成功,這裏假設正確的帳戶密碼是"zhangsan"、"123456" UserModel model = new UserModel { LoginName = UserTestHelper.ExistedLoginName, Password = UserTestHelper.ExistedPassword }; AssertCommon.AssertBoolean(true, UserService.Login(model)); } #endregion #region RegisterTest [TestCategory("註冊場景")] public virtual void 當用戶信息全爲空或帳戶爲空或密碼爲空或帳戶已存在時註冊失敗() { //驗證註冊失敗的場景 AssertCommon.AssertBoolean<UserModel>( new UserModel[] { null, new UserModel(), new UserModel { LoginName = string.Empty, Password = UserTestHelper.NotExistedPassword }, //帳戶爲空 new UserModel { LoginName = UserTestHelper.NotExistedLoginName, Password = string.Empty }, //密碼爲空 new UserModel { LoginName = UserTestHelper.ExistedLoginName, Password = UserTestHelper.NotExistedPassword }, //帳戶已存在 }, false, p => UserService.Register(p)); } [TestCategory("註冊場景")] public virtual void 當帳號密碼均不爲空且帳號未存在則註冊成功而且註冊後的用戶信息與註冊輸入的保持徹底一致() { //驗證註冊成功的場景 //密碼與他人相同也可註冊 UserModel register1 = new UserModel { LoginName = "register1", Password = UserTestHelper.ExistedPassword }; UserModel register2 = new UserModel { LoginName = "register2", Password = UserTestHelper.NotExistedPassword }; UserModel register3 = new UserModel { LoginName = "register3", Password = UserTestHelper.NotExistedPassword, Age = 18 }; AssertCommon.AssertBoolean<UserModel>( new UserModel[] { register1, register2, register3 }, true, p => UserService.Register(p)); //獲取用戶且應與註冊的信息保持一致 UserModel actualRegister1 = UserService.GetModel(register1.LoginName); UserTestHelper.AssertEqual(register1, actualRegister1); UserModel actualRegister2 = UserService.GetModel(register2.LoginName); UserTestHelper.AssertEqual(register2, actualRegister2); UserModel actualRegister3 = UserService.GetModel(register3.LoginName); UserTestHelper.AssertEqual(register3, actualRegister3); } #endregion //該方法可不須要業務人員參與 public virtual void GetModelTest() { AssertCommon.AssertIsNull<string, UserModel>(TestCommon.GetEmptyStrings(), true, p => UserService.GetModel(p)); AssertCommon.AssertIsNull(true, UserService.GetModel(UserTestHelper.NotExistedLoginName)); UserModel actual = UserService.GetModel(UserTestHelper.ExistedLoginName); UserTestHelper.AssertEqual(UserTestHelper.ExistedUser, actual); } }
[TestClass] public class UserServiceTest : BaseUserServiceTest { protected override IUserService UserService { get { return new UserService(this.UserRepository); } } [TestMethod] public override void GetModelTest() { base.GetModelTest(); } [TestMethod] public override void 當用戶信息所有爲空或帳戶爲空或密碼爲空或帳戶錯誤或密碼錯誤或帳戶密碼均錯誤都登錄失敗() { base.當用戶信息所有爲空或帳戶爲空或密碼爲空或帳戶錯誤或密碼錯誤或帳戶密碼均錯誤都登錄失敗(); } [TestMethod] public override void 當帳戶密碼所有正確時登錄成功() { base.當帳戶密碼所有正確時登錄成功(); } [TestMethod] public override void 當用戶信息全爲空或帳戶爲空或密碼爲空或帳戶已存在時註冊失敗() { base.當用戶信息全爲空或帳戶爲空或密碼爲空或帳戶已存在時註冊失敗(); } [TestMethod] public override void 當帳號密碼均不爲空且帳號未存在則註冊成功而且註冊後的用戶信息與註冊輸入的保持徹底一致() { base.當帳號密碼均不爲空且帳號未存在則註冊成功而且註冊後的用戶信息與註冊輸入的保持徹底一致(); } }
4. 這裏我已經在上一篇的基礎上進行了一些重構:框架
在解決方案文件夾「Tests」下新建類庫項目「IdleTest.TDDEntityFramework.TestUtilities」,並添加引用「IdleTest.dll」、「IdleTest.MSTest.dll」
(參考上一篇)和「IdleTest.TDDEntityFramework.Models」。接着在項目下添加類「UserTestHelper」。ide
public class UserTestHelper { public string ExistedLoginName = "zhangsan"; public string ExistedPassword = "123456"; public string NotExistedLoginName = "zhangsan1"; public string NotExistedPassword = "123"; public UserModel ExistedUser { get { return new UserModel { LoginName = ExistedLoginName, Password = ExistedPassword }; } } public UserModel NotExistedUser { get { return new UserModel { LoginName = NotExistedLoginName, Password = NotExistedPassword, Age = 30 }; } } public void AssertEqual(UserModel expected, UserModel actual) { AssertCommon.AssertIsNull(false, expected); AssertCommon.AssertIsNull(false, actual); AssertCommon.AssertEqual<string>(expected.LoginName, actual.LoginName); AssertCommon.AssertEqual<string>(expected.Password, actual.Password); AssertCommon.AssertEqual<int>(expected.Age, actual.Age); } }
5. 再在項目「IdleTest.TDDEntityFramework.ServiceTest」引用剛添加的項目「IdleTest.TDDEntityFramework.TestUtilities」。 post
6. 接着生成並運行測試,在測試資源管理器中單擊右鍵,滑動鼠標到「分組依據」後選中「特徵」,以下圖所示,此時即可以看到比較適合非開發人員的測試方法名。單元測試
使用「[TestCategory]」聲明的測試方法能夠在測試資源管理器中按照特徵來排列。測試
我這裏爲了簡便,把一個細分功能只劃分爲成功與失敗兩個方法,其實應該還能夠劃分得更細些,好比帳戶名爲空登錄失敗、密碼爲空登錄失 this
敗分爲兩個測試方法。固然了,若是在需求並不複雜的狀況下,也能夠不用這麼劃分,好比上述的登錄與註冊需求就很簡單,徹底能夠不用細化,這
裏只做爲演示下罷了。編碼
(本篇將使用與上篇相似的方式完成倉儲層(Repository)開發)
7. 因爲使用Entity Framework Code First,於是需對Model增長一些特性(Attribute)聲明。在編寫如下代碼前需在項
目「IdleTest.TDDEntityFramework.Models」添加引用「System.ComponentModel.DataAnnotations」。 spa
[Table("UserInfo")] public class UserModel { [Key] [MaxLength(50)] public string LoginName { get; set; } [MaxLength(50)] public string Password { get; set; } public int Age { get; set; } }
8. 項目「IdleTest.TDDEntityFramework.Repositories」的變更:添加引用「IdleTest.TDDEntityFramework.Models」;打開程序包管理器控制檯,以下圖所示在默認項目選擇「IdleTest.TDDEntityFramework.Repositories」,並在命令中輸入「Install-Package EntityFramework」(PS 如今才發現Entity Framework已經到了6.0了,不過原有功能應該都還在);
在項目下添加類「SqlFileContext」。
public class SqlFileContext : DbContext { public DbSet<UserModel> Users { get; set; } public SqlFileContext() : base("DefaultConnectionString") { } }
(因爲如下的測試不須要業務人員參與,故我又能夠按照我喜歡的方式來命名單元測試了)
9. 在解決方案文件夾「Tests」下建立單元測試項目「IdleTest.TDDEntityFramework.RepositoryTest」,添加引用 「IdleTest.TDDEntityFramework.TestUtilities」、「IdleTest.TDDEntityFramework.IRepositories」、「IdleTest.TDDEntityFramework.Models」 和 「IdleTest.TDDEntityFramework.Repositories」 以及 「IdleTest」、「IdleTest.MSTest」(相似上一篇);繼續添加「EntityFramework.dll」的引用以下圖所示。
10. 對剛添加的「IdleTest.TDDEntityFramework.Repositories」與「EntityFramework」引用「添加Fakes程序集」。
11. 因爲 「IdleTest.TDDEntityFramework.IRepositories」 有兩個接口 「IUserRepository」、「IRepository」,於是我這裏也建立兩個對應的測試類「RepositoryTest」、「BaseUserRepositoryTest」。
public abstract class RepositoryTest<TEntity, TKey> where TEntity : class { protected abstract IRepository<TEntity, TKey> Repository { get; } public virtual void GetSingleTest() { AssertCommon.AssertIsNull(true, Repository.GetSingle(default(TKey))); } public virtual void InsertTest() { AssertCommon.ThrowException(true, () => Repository.Insert(default(TEntity))); AssertCommon.ThrowException(true, () => Repository.Insert(null)); } }
12. 限於篇幅,本文只對IRepository的「GetSingle」和「Insert」方法進行測試,其餘方法相似,後續完成全部測試再將代碼上傳至http://idletest.codeplex.com/。
13. 繼續編寫類 「BaseUserRepositoryTest」,它與上一篇的 「BaseUserServiceTest」 很是類似。
public abstract class BaseUserRepositoryTest : RepositoryTest<UserModel, string> { protected UserTestHelper UserTestHelper = new UserTestHelper(); protected abstract IUserRepository UserRepository { get;} protected IList<UserModel> ExistedUsers; [TestInitialize] public virtual void Init() { this.ExistedUsers = new List<UserModel> { UserTestHelper.ExistedUser }; } public override void GetSingleTest() { base.GetSingleTest(); AssertCommon.AssertIsNull<string, UserModel>( TestCommon.GetEmptyStrings(), true, p => UserRepository.GetSingle(p)); AssertCommon.AssertIsNull(true, UserRepository.GetSingle(UserTestHelper.NotExistedLoginName)); UserModel actual = UserRepository.GetSingle(UserTestHelper.ExistedLoginName); UserTestHelper.AssertEqual(UserTestHelper.ExistedUser, actual); } public override void InsertTest() { base.InsertTest(); //驗證添加成功的場景 //密碼與他人相同也可添加 UserModel register1 = new UserModel { LoginName = "register1", Password = UserTestHelper.ExistedPassword }; UserModel register2 = UserTestHelper.NotExistedUser; AssertCommon.ThrowException<UserModel>( new UserModel[] { register1, register2 }, false, p => UserRepository.Insert(p)); //獲取用戶且應與註冊的信息保持一致 UserModel actualRegister1 = UserRepository.GetSingle(register1.LoginName); UserTestHelper.AssertEqual(register1, actualRegister1); UserModel actualRegister2 = UserRepository.GetSingle(register2.LoginName); UserTestHelper.AssertEqual(register2, actualRegister2); //驗證添加失敗的場景,使用ThrowException來驗證添加 AssertCommon.ThrowException<UserModel>( new UserModel[] { register1, //不能重複添加 //因爲LoginName對應數據庫字段爲主鍵,故不能爲空 new UserModel { LoginName = string.Empty, Password = UserTestHelper.NotExistedPassword }, }, true, p => UserRepository.Insert(p)); } }
14. 在項目 「IdleTest.TDDEntityFramework.RepositoryTest」 下添加類「UserRepositoryTest」
[TestClass] public class UserRepositoryTest : BaseUserRepositoryTest { protected SqlFileContext dbContext; protected IDisposable TestContext; private IUserRepository userRepository { get { return new UserRepository(dbContext); } } protected override IRepository<UserModel, string> Repository { get { return userRepository; } } protected override IUserRepository UserRepository { get { return userRepository; } } [TestInitialize] public override void Init() { base.Init(); if (dbContext == null) { TestContext = ShimsContext.Create(); //注意使用shim時必須先調用此方法(非全局可以使用using) ShimSqlFileContext context = new ShimSqlFileContext(); ShimDbSet<UserModel> shimDbSet = new ShimDbSet<UserModel>(); shimDbSet.AddT0 = p => { if (this.ExistedUsers.Select(o => o.LoginName).Contains(p.LoginName) || string.IsNullOrEmpty(p.LoginName)) { throw new Exception(); } this.ExistedUsers.Add(p); return p; }; shimDbSet.FindObjectArray = p => { if (p != null && p.Length > 0) { return this.ExistedUsers.FirstOrDefault(o => o.LoginName.Equals(p[0])); } return null; }; context.UsersGet = () => shimDbSet; dbContext = context; } } [TestCleanup] public virtual void Dispose() { this.TestContext.Dispose(); } [TestMethod] public override void InsertTest() { base.InsertTest(); } [TestMethod] public override void GetSingleTest() { base.GetSingleTest(); } }
15. 編寫測試類「UserRepositoryTest」時使用自動生成類生成「UserRepository」,並修改相應代碼使編譯經過
public class UserRepository : IUserRepository { public IEnumerable<UserModel> Get( Expression<Func<Models.UserModel, bool>> filter = null, Func<IQueryable<Models.UserModel>, IOrderedQueryable<Models.UserModel>> orderBy = null, string includeProperties = "") { throw new NotImplementedException(); } public UserModel GetSingle(string id) { throw new NotImplementedException(); } public void Insert(UserModel entity) { throw new NotImplementedException(); } public void Update(UserModel entityToUpdate) { throw new NotImplementedException(); } public void Delete(string id) { throw new NotImplementedException(); } public void Delete(UserModel entityToDelete) { throw new NotImplementedException(); } }
16. 繼續修改直至測試經過(前面說過這裏只對其中兩個方法進行測試)。而後按照上一篇文中的作法,再將UserRepository.cs文件移動到項目 「IdleTest.TDDEntityFramework.Repositories」並添加引用「IdleTest.TDDEntityFramework.IRepositories」,記得要修改命名空間是解決方案編譯經過。
public class UserRepository : IUserRepository, IDisposable { private SqlFileContext dbContext; private DbSet<UserModel> UserModelSet; public UserRepository(SqlFileContext dbContext) { this.dbContext = dbContext; this.UserModelSet = this.dbContext.Users; } public IEnumerable<UserModel> Get( Expression<Func<UserModel, bool>> filter = null, Func<IQueryable<UserModel>, IOrderedQueryable<UserModel>> orderBy = null, string includeProperties = "") { IQueryable<UserModel> query = UserModelSet; if (filter != null) { query = query.Where(filter); } foreach (var includeProperty in includeProperties.Split (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { query = query.Include(includeProperty); } if (orderBy != null) { return orderBy(query).ToList(); } else { return query.ToList(); } } public UserModel GetSingle(string id) { return this.UserModelSet.Find(id); } public void Insert(UserModel entity) { this.UserModelSet.Add(entity); this.dbContext.SaveChanges(); } public void Update(UserModel entityToUpdate) { UserModelSet.Attach(entityToUpdate); dbContext.Entry(entityToUpdate).State = EntityState.Modified; this.dbContext.SaveChanges(); } public void Delete(string id) { var entityToDelete = GetSingle(id); Delete(entityToDelete); } public void Delete(UserModel entityToDelete) { if (dbContext.Entry(entityToDelete).State == EntityState.Detached) { UserModelSet.Attach(entityToDelete); } UserModelSet.Remove(entityToDelete); this.dbContext.SaveChanges(); } public void Dispose() { if (this.dbContext != null) { this.dbContext.Dispose(); } } }
本文囉囉嗦嗦寫了一大堆,其重點在於編寫服務層(業務層)的測試時經過改變一些編碼習慣以便於業務人員的參與;其次則是UserRepositoryTest中的Init方法,對DbContext和DbSet進行了模擬,而我本身編寫的繼承DbContext的SqlFileContext類將不會被測試。
其實再寫本文前我也沒有編寫相似的單元測試,算是我的邊實踐邊作的筆記,感受對數據倉儲(或者說數據訪問層)的測試作到面面俱到仍然 仍是有難度。甚至我認爲這種只對Entity Framework框架提供的操做進行封裝的測試可能不太有必要。