開發工具:Visual Studio 2012html
測試庫:Visual Studio 2012自帶的MSTest前端
DI框架:Unity 框架
數據持久層:Entity Frameworkide
前端UI:ASP.NET MVC 4.0工具
需求:我這裏假設只知足兩個功能,一個用戶註冊,另外一個則是登錄的功能,藉助於一些DDD思想,我將從領域層(或者常說的BLL)開始開發,固然每一層都是採用TDD,按我喜歡的作法就是「接口先行,測試驅動」,不廢話,直奔主題吧。post
有關VS2012的單元測試請參見《VS2012 Unit Test 我的學習彙總(含目錄)》單元測試
有關測試中使用的IdleTest庫請參見http://idletest.codeplex.com/學習
1. 建立空白解決方案「IdleTest.TDDEntityFramework」,新建解決方案文件夾「Interfaces」,並在文件夾內建立兩個項目 「IdleTest.TDDEntityFramework.IRepositories」 和 「IdleTest.TDDEntityFramework.IServices」。開發工具
2. 直接在解決方案下建立類庫項目 「IdleTest.TDDEntityFramework.Services」、「IdleTest.TDDEntityFramework.Models」 和 「IdleTest.TDDEntityFramework.Repositories」測試
3. 在解決方案下建立MVC4項目"IdleTest.TDDEntityFramework.MvcUI"做爲最終的UI,我這裏選擇空模板,解決方案初始結構初始結構圖以下
4. 把全部類庫項目中自動生成的「Class1.cs」文件刪除。
5. 使用Visio畫出解決方案中各項目的關係(以下圖),這圖畫的是項目關係,實際上這些項目內的類也都遵循這樣的關係。例如本項目只有一個Model,即UserModel,那麼「IdleTest.TDDEntityFramework.IRepositories」下就相應將類命名爲「IUserRepository」,「IdleTest.TDDEntityFramework.IServices」對應「IUserService」,以此類推,非接口則去掉前綴「I」。這是我我的的一些習慣,每一個人可能命名方式可能不太同樣,這很正常,可是若是是超過一我的來共同開發,則應將規範統一,俗話說「約定優於配置」嘛。
6. 這裏只是本身演練TDD的Demo而已,將不使用「UnitOfWork」,其餘也可能會缺乏很多功能,由於不低不在於Entity Framework或MVC等等,而關注的只是單元測試驅動開發罷了。
7. 在「IdleTest.TDDEntityFramework.Models」下添加類「UserModel」。
public class UserModel { public string LoginName { get; set; } public string Password { get; set; } public int Age { get; set; } }
8. 分別在項目「IdleTest.TDDEntityFramework.IRepositories」和「IdleTest.TDDEntityFramework.IServices」下添加引用「IdleTest.TDDEntityFramework.Models」,並分別添加接口「IUserRepository」、「IRepository」和「IUserService」。
public interface IUserRepository : IRepository<UserModel, string> { }
public interface IRepository<TEntity, TKey> where TEntity : class { IEnumerable<TEntity> Get( Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = ""); TEntity GetSingle(TKey id); void Insert(TEntity entity); void Update(TEntity entityToUpdate); void Delete(TKey id); void Delete(TEntity entityToDelete); }
public interface IUserService { bool Login(UserModel model); bool Register(UserModel model); UserModel GetModel(string loginName); }
那麼藉助DDD的一些思想,這裏的IUserService體現着功能需求,Service這層的代碼徹底由業務需求肯定,於是IUserService只編寫了三個方法。而Repository這層則不去關心業務,只是常規性的公開且提供一些方法出來,這在不少項目中幾乎都是肯定,孤兒IRepository也就天然而然具備了增刪改查的功能了。
9. 開始涉及單元測試,建立解決方案文件夾「Tests」,並在該文件夾下建立單元測試項目「IdleTest.TDDEntityFramework.ServiceTest」,添加引
用「IdleTest.TDDEntityFramework.IRepositories」、「IdleTest.TDDEntityFramework.IServices」、「IdleTest.TDDEntityFramework.Services」、「IdleTest.TDDEntityFramework.Models」,緊接着對「IdleTest.TDDEntityFramework.IRepositories」添加「Fakes程序集」(有關Fakes可參照《VS2012 Unit Test——Microsoft Fakes入門》)。
10. 在解決方案物理路徑下建立文件夾「libs」,並將「IdleTest」中相關dll拷貝進去。接着在項目「IdleTest.TDDEntityFramework.ServiceTest」添加引用,在「引用管理器」中單擊「瀏覽」按鈕,找到剛剛建立的「libs」文件夾,並添加下圖所示引用。有關IdleTest可參照從http://idletest.codeplex.com下載編譯。
11. 我將在剛添加的測試項目中編寫一個針對「IUserService」的測試基類「BaseUserServiceTest」(關於對接口的測試能夠參照《VS2012 Unit Test —— 我對接口進行單元測試使用的技巧》)。
using IdleTest; using IdleTest.MSTest; using IdleTest.TDDEntityFramework.IServices; using IdleTest.TDDEntityFramework.IRepositories.Fakes; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; using IdleTest.TDDEntityFramework.Models; using IdleTest.TDDEntityFramework.IRepositories; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace IdleTest.TDDEntityFramework.ServiceTest { public abstract class BaseUserServiceTest { protected string ExistedLoginName = "zhangsan"; protected string ExistedPassword = "123456"; protected string NotExistedLoginName = "zhangsan1"; protected string NotExistedPassword = "123"; 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> { new UserModel { LoginName = ExistedLoginName, Password = ExistedPassword } }; } public virtual void LoginTest() { //驗證登錄失敗的場景 AssertCommon.AssertBoolean<UserModel>( new UserModel[] { null, new UserModel(), new UserModel { LoginName = string.Empty, Password = ExistedPassword }, //帳戶爲空 new UserModel { LoginName = ExistedLoginName, Password = string.Empty }, //密碼爲空 new UserModel { LoginName = ExistedLoginName, Password = NotExistedPassword }, //密碼錯誤 new UserModel { LoginName = NotExistedLoginName, Password = NotExistedPassword }, //帳戶密碼錯誤 new UserModel { LoginName = NotExistedLoginName, Password = ExistedLoginName } //帳戶錯誤 }, false, p => UserService.Login(p)); //帳戶密碼正確,驗證成功,這裏假設正確的帳戶密碼是"zhangsan"、"123456" UserModel model = new UserModel { LoginName = ExistedLoginName, Password = ExistedPassword }; AssertCommon.AssertEqual<bool>(true, UserService.Login(model)); } public virtual void RegisterTest() { //驗證註冊失敗的場景 AssertCommon.AssertBoolean<UserModel>( new UserModel[] { null, new UserModel(), new UserModel { LoginName = string.Empty, Password = NotExistedPassword }, //帳戶爲空 new UserModel { LoginName = NotExistedLoginName, Password = string.Empty }, //密碼爲空 new UserModel { LoginName = ExistedLoginName, Password = NotExistedPassword }, //帳戶已存在 }, false, p => UserService.Register(p)); //驗證註冊成功的場景 //密碼與他人相同也可註冊 UserModel register1 = new UserModel { LoginName = "register1", Password = ExistedPassword }; UserModel register2 = new UserModel { LoginName = "register2", Password = NotExistedPassword }; UserModel register3 = new UserModel { LoginName = "register3", Password = NotExistedPassword, Age = 18 }; AssertCommon.AssertBoolean<UserModel>( new UserModel[] { register1, register2, register3 }, true, p => UserService.Register(p)); //獲取用戶且應與註冊的信息保持一致 UserModel actualRegister1 = UserService.GetModel(register1.LoginName); AssertCommon.AssertEqual<string>(register1.LoginName, actualRegister1.LoginName); AssertCommon.AssertEqual<string>(register1.Password, actualRegister1.Password); AssertCommon.AssertEqual<int>(register1.Age, actualRegister1.Age); UserModel actualRegister2 = UserService.GetModel(register2.LoginName); AssertCommon.AssertEqual<string>(register2.LoginName, actualRegister2.LoginName); AssertCommon.AssertEqual<string>(register2.Password, actualRegister2.Password); AssertCommon.AssertEqual<int>(register2.Age, actualRegister2.Age); UserModel actualRegister3 = UserService.GetModel(register3.LoginName); AssertCommon.AssertEqual<string>(register3.LoginName, actualRegister3.LoginName); AssertCommon.AssertEqual<string>(register3.Password, actualRegister3.Password); AssertCommon.AssertEqual<int>(register3.Age, actualRegister3.Age); } public virtual void GetModelTest() { AssertCommon.AssertIsNull<string, UserModel>(TestCommon.GetEmptyStrings(), true, p => UserService.GetModel(p)); AssertCommon.AssertIsNull(true, UserService.GetModel(NotExistedLoginName)); UserModel actual = UserService.GetModel(ExistedLoginName); AssertCommon.AssertEqual<string>(ExistedLoginName, actual.LoginName); AssertCommon.AssertEqual<string>(ExistedPassword, actual.Password); } } }
BaseUserServiceTest類自己不會具備任何測試,只有子類去繼承它,且實現抽象屬性「UserService」、Override相應的測試方法(LoginTest、RegisterTest、GetModelTest)並聲明「TestMethod」特性後才能進行測試。
12. 在測試項目再編寫類UserServiceTest,繼承BaseUserServiceTest。
[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 LoginTest() { base.LoginTest(); } [TestMethod] public override void RegisterTest() { base.RegisterTest(); } }
因爲父類已作好了相應的測試代碼,此時編寫UserServiceTest就有點一勞永逸的感受了。
注意在實現「UserService」屬性時,編寫以下圖所示代碼後按「Alt+Shift+F10」在彈出的小菜單中選中「爲UserService生成類」回車,這時發現它生成在了咱們的測試項目中,我暫時不會去理會這些,如今最要緊的是我須要在最短期最少代碼量上使得個人測試經過。
接着去修改剛生成的UserService類。
public class UserService : IUserService { private IUserRepository userRepository; public UserService(IUserRepository userRepository) { // TODO: Complete member initialization this.userRepository = userRepository; } public bool Login(UserModel model) { throw new NotImplementedException(); } public bool Register(UserModel model) { throw new NotImplementedException(); } public UserModel GetModel(string loginName) { throw new NotImplementedException(); } }
13. 生成以後打開「測試資源管理器」稍等幾秒便可發現三個須要測試的方法呈現了。此時測試固然都是所有不經過。繼續往下修改UserService,直至測試經過。
public class UserService : IUserService { private IUserRepository userRepository; public UserService(IUserRepository userRepository) { // TODO: Complete member initialization this.userRepository = userRepository; } #region IUserService成員 public bool Login(UserModel model) { if (!IsValidModel(model)) { return false; } IList<UserModel> list = userRepository.Get(p => p.LoginName == model.LoginName && p.Password == model.Password).ToList(); return list != null && list.Count > 0; } public bool Register(UserModel model) { if (!IsValidModel(model)) { return false; } if (GetModel(model.LoginName) != null) { return false; } userRepository.Insert(model); return true; } public UserModel GetModel(string loginName) { if (!string.IsNullOrEmpty(loginName)) return userRepository.GetSingle(loginName); return null; } #endregion private bool IsValidModel(UserModel model) { return model != null && !string.IsNullOrEmpty(model.LoginName) && !string.IsNullOrEmpty(model.Password); } }
14. 此時測試已經過,查看代碼覆蓋率,雙擊」UserService「下未達到100%覆蓋率的行(以下圖所示)能夠查看哪些代碼還沒有覆蓋,而後酌情再看是否須要增長或修改代碼以使覆蓋率達到100%,我這裏分析當前未覆蓋的對項目沒有什麼影響,故再也不修改。
15. 最後將UserService類剪切到項目」IdleTest.TDDEntityFramework.Services「,添加引用,修改相應命名空間。
再次運行測試並順利經過,那麼這一階段的開發與單元測試均大功告成。
上述過程簡言之,就是先搭建VS解決方案的項目結構,而後編寫Model(此無需測試,也是整個項目傳遞數據的基本),再寫項目須要的接口,接着針對接口編寫單元測試, 最後纔是編寫實現接口的類代碼。
對於實現接口的類中的一些方法(如「UserService」類的「IsValidModel」方法)我並無針對它編寫測試,首先它是一個私有方法(關於私有方法需不須要測試的爭論貌似如今尚未統一的結論,鄙人能力有限,不敢妄加評價);其次即便它是一個public方法,我也仍然不會去測試它,由於它只是爲「IUserService」接口成員服務的,或者說該方法本來就不須要,只是我寫代碼中重構出來,編寫完UserService我只關心該類中的「IUserService」接口成員,因此…… 其實,這裏也能夠經過代碼覆蓋率看到,即便沒有專門對「IsValidModel」方法編寫相應測試,可是它的覆蓋率仍然是100%,我不能肯定私有方法到底要不要測試,可是在這裏我不測「IsValidModel」方法確定沒有錯。
測試基類「BaseUserServiceTest」是針對「IUserService」接口編寫的,而它的子類貌似什麼都不作,我之因此這麼寫,只是爲了之後若是有新的類實現「IUserService」接口 時,我仍然只須要簡單的添加「BaseUserServiceTest」的一個子類,就能夠完成測試,文中貌似也提到,有種一勞永逸的感受,除非接口改變,不然對類的修改等等基本都不會影響 到原有測試。這樣就足以保證了之後修改bug、代碼重構或需求變化時對代碼修改後仍能。
因爲使用了依賴注入,故而測試時就能夠隔離依賴,文中Service層本來是依賴Repository,可是我這裏在未具體實現Repository前都不會影響對Service層的開發與測試。
TDD前期工做量比較大,可是對於後期代碼(例如總體測試修改bug、代碼重構或需求變化時對代碼修改)質量的保證是很是可靠的。
未完待續。。。。。。