I 测试驱动开发入门
一 通向测试驱动开发之路
敏捷方法简介:
- 个人与互动 重于 流程与工具
- 可用的软件 重于 详尽的文件
- 与客户合作 重于 合约协商
- 响应变化 重于 遵循计划
敏捷注重交流沟通,应对变化。
著名的敏捷开发方法:
- Scrum
- 极限编程(XP)
- 功能驱动开发
- Clear Case
- 自适应软件开发(Adaptive Software Development)
二 单元测试简介
单元测试就是针对一个工作单元设计的测试。一个工作单元是指对一个方法的一个要求。
单元测试的共同特征:
- 与其他代码隔离
- 与其他开发人员隔离
- 有针对性
- 可重复
- 可预测
其它类型的测试:
单元测试框架:NUnit等
与模拟对象分离:虚拟、伪对象(Mock)、存根(Stub)和模拟(Shim)
模拟对象(Mock Object)用来在应用程序中代表那些其他组件,有时代表外部资源。利用这些对象测试代码时,不需要担心与外部资源进行交互的后果。
在TDD中使用模拟时,应当了解以下概念:
- 依赖注入(Dependency Injection)—— 对类进行内部实例化时,不是静态创建该类所依赖的对象,而是向其提供这些对象的一些实例,这些实例符合该依赖项所需要的接口。
- 为接口设计,而不是为实现设计—— 在将另一个类或资源用作依赖项时,应当关注的不是他如何执行自己的任务,而是关心其接口是什么。同理,在设计和构建服务的类时,应当使用接口来抽象API中的功能。
- 尝试限制依赖项
- 不要模拟私有方法—— 如果正在编写一个测试,而且代码依赖于另一个类,应当仅模拟公共方法(而且是仅模拟哪些直接使用的公共方法)。
- 不要欺骗——不要通过模拟来走一些捷径。
推荐的模拟框架:Moq
三 重构速览
TDD项目的生命周期:红灯、绿灯、重构。也就是说:开始是一个未通过的测试,因为还没有实现能够通过这一测试的逻辑;然后实现此测试,使其得以通过;最后重构代码,对其加以改进,同时不会影响测试结果。
代码测试覆盖率度量中应当始终包含所有
功能代码。这些功能部分几乎包含了除以下对象之外的所有内容:实体对象、数据传输对象,以及任何以包含数据为惟一目的的对象或结构。
OOP(Object Oriented Programming)的三条一般原则:封装、继承、多态。
面向对象设计的SOLID原则:
- 单一职责原则(Single Responsibility Principal):每个方法或类应当有且仅有一个改变的理由。
- 开放/封闭原则(Open/Close Principal):软件(方法或类等)应当开放扩充且关闭修改。
- 里氏替换原则(Liskov Substitution Principal):用超类代替应用程序中使用的对象时,应当不会破坏应用程序。也通常被称为“契约式设计(design by contract)”
- 接口分离原则(Interface Segregation Principal):不应当强制客户端依赖岂不使用的接口。应该专门针对客户端的需要,创建更小、更精细的接口。
- 依赖倒置原则(Dependency Inversion Principal):代码应当依赖于抽象概念,而不是具体实现;这些抽象不应当依赖于细节;而细节应当依赖于抽象。
DRY原则(Don't Repeat Yourself):避免重复代码。
代码异味(Code Smell):
- 重复代码和相似类
- 大型类和大型方法
- 不恰当的注释
- 不恰当的命名
- 特征依赖:如果一个类过多地使用另一个类的方法,那就说它有些“特征依赖(feature envy)”
- If/Switch过多
- Try/Catch过多
重构方法:
- 析取类或接口:将一个类分割为更小、针对性更强的类,或者从中析取出一系列更精细的接口。
- 析取方法
- 重命名变量、字段、方法和类
- 封装字段
- 用多态替换条件
- 允许类型推断
四 模拟外部资源
1. 依赖注入介绍
依赖注入框架:Ninject
Ninject需要类来存储其用于生成依赖项具体实例的规则,这些类被称为模块。
在被重写的Load方法中定义一些规则,用来创建相应的类。
如果类在构造函数形参中所需要的是Ninject
可以提供的,Ninject会自动创建一个实例并作为形参传递给该类。
如果类在构造函数形参中所需要的是Ninject
所不能提供的,Ninject允许为特定接口创建Provider类。
使用Ninject的一个例子:
public BussinessService()
{
private ILoggingComponent _loggingComponent;
private IDatabaseAccessComponent _dataAccessComponent;
...
public BussinessService(ILoggingComponent, loggingComponent, IDatabaseAccessComponent databaseAccessComponent)
{
_loggingComponent = loggingComponent;
_dataAccessComponent = databaseAccessComponent;
...
}
}
public class CoreModule:NinjectModule
{
public override Load()
{
Bind().To();
Bind().To();
Bind().ToProvider(new DataAccessComponentProvider);
}
}
public class DataAccessComponentProvider:Provider
{
protected override IDataAccessComponent CreateInstance(IContext context)
{
var databaseConnectionString = .....
return new DatabaseAccessComponent(databaseConnectionString);
}
}
Client端:
BussinessService actual;
var kernel = new StandardKernel(new CoreModule()); //StandardKernel comes from Ninject framework
actual = kernel.Get();
2. 抽象数据访问层 - Repository模式
存储库模式要求所有的数据访问都封装在一个存储库对象中,业务域类将用他来执行所有的持久化工作。
实体类
public class Persion
{
public int Id {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
}
存储库类
public interface IPersonRepository
{
Person GetPerson(int personId);
}
public class PersonRepository : IPersonRepository
{
pulbic Person GetPerson(int personId)
{
...
}
}
业务域类
public interface IPersonService
{
Person GetPerson(int personId);
}
public class PersonService : IPersonService
{
private readonly IPersonRepository _personRepository;
public PersonService(IPersonRepository personRepository)
{
_personRepository = personRepository;
}
public Persone GetPerson(int personId)
{
return _personRepository.GetPerson(personId);
}
}
Client端可以使用Mock进行单元测试:
var personRepositoryMock = new Mock();
personRepositoryMock.Setup(pr => pr.GetPerson(1)).Returns(new Person{Id = 1, FirstName = "Bob", LastName = "Smith"});
var personService = new PersonService(personRepositoryMock.Object);
II 将基础知识变为行动
五 敏捷开发流程
一、定义项目
1. 开发项目综述——是对应用程序业务需要的高级描述。它描述了应用程序的整体目标,部分关键工作流,以及应用程序的主要用户角色和类型。用于提供业务需求的骨架结构。
2. 定义目标环境——不仅要考虑应用程序的部署环境,还要考虑将如何构建应用程序,并将针对它收集哪些类型的基础设施架构需求。例如是基于Web的应用程序,or在用户的桌面上运行,or在盈动平台上运行?该应用程序是否有多个层?希望在峰值时有多少并发用户?应用程序需要提供何种响应时间?应用程序是否需要扩展?
3. 选择应用程序所需的技术——确定应用程序平台(.NET, J2EE...),以及所使用的框架和工具。
二、定义用户情景
用户情景——表示应用程序的业务需求,定义了应用程序的规则和工作流,它应当描述业务用户与应用程序之间的预期交互。
1. 收集情景——收集创建这些情景所需的信息,然后将其与业务比对已验证它们。3种常见的技术(主动->被动):跟班、客户会谈、联合应用程序设计(JAD)会话。当开发团队完成了满足用户情景所必需的功能之后,应当立即可供用户测试。
2. 确定待办事项表——代办事项表列出了为完成该应用程序需要做的工作(用户情景被分解为一些可以分配给开发人员来完成的功能)。
三、敏捷开发过程
大多数敏捷过程都特别重视一条思想:在很短的迭代过程中完成易于管理和理解的小型工作单元;尽可能快速、频繁地向用户提交软件。
1. 估计——估计不确定性锥形:在项目开始时所做工作评估的不准确因素可能为4。这意味着完成一项功能所需要的实际工作可能是估计值的25%,也可能是估计值的400%。
一种更你符合逻辑的做法是定期对待办事项表中的功能进行重新估计。
2. 迭代——敏捷方法鼓励采用短时迭代。
- “零次迭代”:设置迭代,是第一次迭代,通常是在收集了主要的非功能需求并定义了用户情景之后,准备结束需求收集阶段。在这一期间创建开发环境,设置将会用到的应用服务器,设置OA环境并配置连续迭代服务器(CI负责编译应用程序,并运行所有单元测试和集成测试)。
3. 团队内部交流——建立项目的有关信息,共团队参考,减少低效冗长的会议。
4. 第一次迭代——在开发人员选择了要开始处理的功能之后,下一步就是阅读用户情景,并与业务分析师、项目经理等业务专家会谈,对功能进行回顾,以确保自己能够从业务的角度理解功能或用户情景。
5. 迭代中的测试——自动化的测试,保证所有单元测试和集成测试能够通过。
6. 结束迭代——迭代结束后的周一或周二安排一场有业务人员参加的“展示——告知”会议,向客户展示在上一个迭代周期内完成的工作,发现缺陷和修改。缺陷总处于最高优先级。改变指的是应用程序业务需求因为某种原因不够准确,这些新的或要修改的用户情景需要验证,分解为功能,还必须预计以工作量,然后被添加到代办事项表中,安排在随后的迭代周期中完成。
六 实现第一个用户情景
在计划工作时,以合理方式安排各项功能的开发顺序:从应用程序的核心开始,向外层发展。在将用户情景分解为功能时,一定要让他们保持小型化、相互隔离、可以测试。
在创建单元测试时,采用一致的标准为类、方法和变量命名,为它们起的名字应当是有意义的。
TDD要求仅编写能使测试通过的最少量代码。
一定要从多个方向进行测试,要测试方法的边缘情景:输入哪些位于期望边界的参数、超出边界的取值,以及预期之外的取值和条件。
在拥有一套通过的测试之后,可以对代码进行重构,使它更优化、可读性更强、更易于维护,牢记SOLID原则。
七 集成测试
单元测试确保各个组件满足在功能和用户情景中列出的业务需求,重要的一点是让单元测试以及执行这些测试的代码与其他组件或外部资源隔离。
集成测试用来验证正在开发的各个应用程序组件和外部资源能够正确地协同工作。
端对端测试是一种特殊的集成测试,用来验证应用程序代码可以根据应用程序的用户情景来执行整个业务工作流。
早集成、常集成。