【前言】

开发工具:Visual Studio 2012

测试库:Visual Studio 2012自带的MSTest

DI框架:Unity

数据持久层:Entity Framework

前端UI:ASP.NET MVC 4.0

需求:我这里假设只满足两个功能,一个用户注册,另一个则是登陆的功能,借助于一些DDD思想,我将从领域层(或者常说的BLL)开始开发,当然每一层都是采用TDD,按我喜欢的做法就是“接口先行,测试驱动”,不废话,直奔主题吧。

有关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; }
}

UserModel

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);
}

IRepository

    public interface IUserService
{
bool Login(UserModel model); bool Register(UserModel model); UserModel GetModel(string loginName);
}

IUserService

  那么借助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 = ""; protected string NotExistedLoginName = "zhangsan1"; protected string NotExistedPassword = ""; 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 = };
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

  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

  由于父类已做好了相应的测试代码,此时编写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();
}
}

UserService

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 > ;
} 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);
}
}

UserService

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、代码重构或需求变化时对代码修改)质量的保证是非常可靠的。

  未完待续。。。。。。

使用IdleTest进行TDD单元测试驱动开发演练(1)的更多相关文章

  1. 使用IdleTest进行TDD单元测试驱动开发演练(3) 之 ASP.NET MVC

    一.[前言] (1)本文将用到IOC框架Unity,可参照<Unity V3 初步使用 —— 为我的.NET项目从简单三层架构转到IOC做准备>(2)本文的解决方案是基于前述<使用I ...

  2. 使用IdleTest进行TDD单元测试驱动开发演练(2)

    [前言] 1. 有关上篇请参见<使用IdleTest进行TDD单元测试驱动开发演练(1)>,有关本篇用到Entity Framework Code First请参见<使用NuGet助 ...

  3. TDD单元测试驱动

    使用IdleTest进行TDD单元测试驱动开发演练(2)   [前言] 1. 有关上篇请参见<使用IdleTest进行TDD单元测试驱动开发演练(1)>,有关本篇用到Entity Fram ...

  4. TDD测试驱动开发

    TDD测试驱动开发 一.概念 TDD故名思意就是用测试的方法驱动开发,简单说就是先写测试代码,再写开发代码.传统的方式是先写代码,再测试,它的开发方式与之正好相反. TDD是极限编程的一个最重要的设计 ...

  5. 我看TDD测试驱动开发

    今天在实验室给大家介绍了一下TDD和Docker,大家对TDD都比较感兴趣,包括老板,也问了一些问题. 还是从头来说TDD吧,TDD作为敏捷开发领域的领头军,充满魅力,同时也充满争议.一切从三大军规说 ...

  6. 行为驱动开发BDD和Cucunber简介

    测试驱动开发(TDD) 1.测试驱动开发,即Test-Driven Development(TDD),测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论.TDD的原理是在开发功能代码之前 ...

  7. 行为驱动开发BDD概要

    BDD脱胎于TDD 行为驱动开发(Behavior-Driven Development,简称BDD),是在测试驱动开发(Test-Driven Development,TDD)基础上发展而来的一种软 ...

  8. 测试计划驱动开发模式 TPDD:一种比 TDD 更友好的开发模式

    相信大部分开发团队都在使用TDD,并且还有很多开发团队都 对外声明 在使用 TDD 开发模式. 之所以说是“对外声明”,是因为很多开发团队虽然号称使用的是 TDD 开发模式,实际开发过程中却无法满足 ...

  9. Scrum敏捷软件开发之技术实践——测试驱动开发TDD

    重复无聊的定义 测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法.它要求在编写某个功能的代码之前先编写测试代码,然后只编写 ...

随机推荐

  1. Linux不能上网ping:unknown host问题怎么解决?

    Linux不能上网提示ping:unknown host 检查步骤 Linux系统跟windows平台有所不同的是,为了更好的做网络服务应用.Linux下多用于网络服务器,而且操作界面是字符界面.对于 ...

  2. JS案例之3——倒计时

    利用简单的数字累加循环模拟倒计时的效果,逻辑比较简单.如果大牛们有更好的办法欢迎补充. 这种效果经常用于在规定的时间做某件事.比如在1分钟之后重新发送验证码等. 案例演示: 源代码如下: <!D ...

  3. FastClick 填坑及源码解析

    最近产品妹子提出了一个体验issue —— 用 iOS 在手Q阅读书友交流区发表书评时,光标点击总是不好定位到正确的位置: 如上图,具体表现是较快点击时,光标总会跳到 textarea 内容的尾部.只 ...

  4. NodeJS的代码调试和性能调优

    本文转自我的个人博客. NodeJS 自 2009 年显露人间,到现在已经六个年头了,由于各种原因,中间派生出了个兄弟,叫做 iojs,最近兄弟继续合体,衍生出了 nodejs4.0 版本,这东西算是 ...

  5. Java下好用的开源库推荐

    作者:Jack47 转载请保留作者和原文出处 欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 本文想介绍下自己在Java下做开发使用到的一些开源的优秀编程库,会不定 ...

  6. 修改Tomcat响应请求时返回的Server内容

    HTTP Server在响应请求时,会返回服务器的Server信息,比如 Tomcat 7 的Header是: 这东西其实会给一些别有用心之人带来一定的提示作用:为安全起见,我们一般会建议去掉或修改这 ...

  7. ABP(现代ASP.NET样板开发框架)系列之3、ABP分层架构

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之3.ABP分层架构 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)” ...

  8. springMvc的搭建

    学习SpringMVC--从HelloWorld开始   前言: 时隔十二年,中国女排最终过关斩将,用3:1的成绩证明了自己的实力,霸气夺冠,为中国赢得了一枚意义非常的金牌.这是一次全民的狂欢,一场视 ...

  9. EMC与地之重新认识地

    记得在Mark的培训中,他手上拿了一个无线鼠标,然后问了一个很有意思的问题:“这个无线鼠标的地在哪里?同样,我们的手机没有和任何大地有接 触,那么这个地又在哪里呢?”这个问题确实很有意思,也确实让人很 ...

  10. Webpack从入门到上线

    webpack是目前一个很热门的前端打包工具,官网说得很清楚,webpack的出现就是要把requirejs干掉.同时它还提供了十分便利的本地开发的环境.网上并不容易找到一个讲解得比较详细完整的教程, ...