开发工具: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、代码重构或需求变化时对代码修改)质量的保证是很是可靠的。
未完待续。。。。。。