开始以前程序员
本文侧重讲述如何在iOS程序的开发过程当中使用单元测试。使用Xcode自带的OCUnit做为测试框架。编程
1、单元测试概述安全
单元测试做为敏捷开发实践的组成之一,其目的是提升软件开发的效率,维持代码的健康性。其目标是证实软件可以正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,可是很显然这与下降软件开发成本这一目的背道而驰)。它是对软件质量的一种保证,例如重构以后咱们须要保证软件产品的正常运行。网络
不少人认为编写单元测试没有用是认为单元测试并不能保证必定能减小bug发生的概率,而因为编写单元测试必定会花费必定的时间与精力,于是必然的会增长成本。客观的说,形成这种缘由很大的程度上是程序员的水平不够高。我认为使用使用单元测试带来巨大好处的必要条件以下所示:架构
程序员自己的编程水平--是否有较多的代码经验,是否熟练掌握重构框架
程序员对项目的认知--是否能正确理解软件或模块的需求异步
项目质量--是否稳定,是否长期多版本,是否须要应对较多变化ide
若是程序员水平较高,对需求理解较为清晰,项目须要面对较多的变化,那么毫无疑问单元测试对于软件很是有益。假如软件功能简单且开发周期短,不须要进行复杂的维护工做,那么单元测试的意义并不大。函数
优秀的单元测试实践的好处:单元测试
好的单元测试就是一份好的文档,而且比文档更能为程序员所接受,它直接描述了测试员对受测代码的结果所持的预期。
当代码由别人维护时(或本身进行重构时),经过单元测试的约束,才能保证在加入新功能或修改旧功能时代码的正确性。
因为单元测试的自动化执行,保证了在整个开发流程中代码都会被测试,这很是符合XP思想。
保证在面对软件功能的变化时,程序员能够较为放心的进行代码重构,而没必要担忧是否破坏了原有功能。
好的单元测试能够下降bug数量,而对于项目管理来讲,修改bug这个过程是没法制定计划的,可使软件的开发流程更容易掌控。
能够由老程序员编写描述某个类行为的测试,以此指导新程序员对类的编码。
……
好处还有不少,但最重要的一点就是保证了软件质量的同时,因为减小bug和应对变化形成的回归bug的产生等,提升了劳动生产率。并且,在敏捷流程中,使用单元测试是必须掌握的手段,不然就没发保证重构的正确性,从而形成代码没法面对变化。
2、iOS的单元测试概述
刚接触客户端编程时,我在很长一段时间内都想不通对于客户端程序如何编写单元测试。单元测试本质上说白了就是用一些断言来断定结果,而这种方式是如何应用到具备复杂交互的界面测试上来的呢?
咱们要作的就是将客户端代码转化为易于测试的代码。什么样的代码易于测试呢?它至少是这样的:
一、被测方法须要产生可测量的结果。
二、类之间的关系应该是松耦合的。
其中第一条是必要条件。使用断言这种形式指明了测试的方法最终要形成某些能够度量的结果。于是,咱们须要尽可能的将展现和业务逻辑分离开来。展现的代码是无法测试的,例若有的方法只是播放动画。而业务逻辑最终都会形成一些数据的改变,这是容易测试的。
大略的讲,做为一个iOS程序员来讲,首先要了解一个叫作MVC的模式。这个模式定义了Cocoa Touch框架的整体结构。在iOS程序中,咱们也须要按照这种模式进行界面代码的编写。这样设计出来的类具备较好的结构,且比较适合于作单元测试。
而后必定要懂得不停重构代码,这样咱们才能使代码不停地改善,不停地变得更加适合单元测试。
有一些框架能够帮助你们更好的测试,分别是OCUnit、GTM、GHUnit、CATCH、OCMock,但目前对我来讲,OCUnit足够用了。做为苹果官方提供的测试框架,它最大的优势就是简单易用。
3、单元测试实践
下面是一些我所理解的单元测试中比较好的实践。
顾名思义,单元测试面向的对象是单元,这个专有名词源自编译器领域的术语“编译单元”。在面向过程当中,指的是函数,而在面向对象中,指的一般就是“类”。于是,每一个功能类都应该提供对应的单元测试。
实践1 每一个功能类都应提供单元测试,且每个测试类,只依赖于其要测试的受测类。使用伪造对象能够避免对其余类的依赖。
解释 保证一个测试类只关注一个被测类,当测试不经过时,就能迅速的定位到是谁发生了错误,而不会受到其余类的干扰。
简单的数据类等能够不提供,可是要保证该测试的都要覆盖到。并不存在一种合适的度量指标能够量化地判断某种单元测试方案是否成功。经常使用的标准(代码覆盖率和成功执行的测试用例数)均可以在受测软件的质量不变的状况下人为的修改(做假)。固然,在没法确保程序员素质的状况下,做为没有办法的办法,使用这种标准也是能够的(或者无奈的说,必须的)。单元测试须要程序员本身把关,关注哪些功能确实须要测试覆盖。这也就是前面所说的一些程序员不相信单元测试能够提升生产率的理由--它更多的依赖于程序员的素质,这是没有保证的。但一样的,因为敏捷是一种以人为本的思想实践,于是这种行为彷佛又是一种必然。
实践1.1 使用伪造类避免对其余类的依赖。
解释 避免依赖的一种手段。
例如,某个被测的方法声明是这样的:
-(void)xxxx:(Person *)person;
若是测试时传入Person的话,就形成了测试类依赖于两个类。当因为person中的错误引起测试不经过时,就不能迅速的定位到受测类中是否有问题。遇到这种状况,就可使用伪造类。假如方法中只使用了person的一个属性name,那么能够将方法名重构为
-(void)xxxx:(id)person;(此处id有待商榷,只是这样作最简单)
而后在单元测试的target中添加只包含name属性的fakePerson来做为伪造类。这样,一旦发生错误就能够迅速的推测出错误的来源。
实践1.2 使用伪造环境避免其余环境的干扰。
解释 适合于异步的方法测试。
很常常遇到的一种状况是测试有网络环境的代码。因为异步的存在,这会形成测试代码很差写。一种简单的解决方法是,咱们假定网络必定是通畅的,则咱们测试的代码将分为两部分,即拼装发送功能和接收解析功能。假如发送和接收功能各自都能经过测试,那么咱们大约能够肯定这个异步方法的正确性。另外一种方法是使用GHUnit,它支持异步代码的测试。
实践2 测试用例(方法)名应该是自解释的且是独立的。
解释 基本功。
若是被测试类的名称是XXX,那么测试类能够命名为XXXTests。而对于其中要测试的功能,命名应该是自解释的。这能够在发现错误时尽快的定位问题所在。例如,若是某个属性obj应该是非空的,那么咱们能够将其命名为:
-(void)testObjNotNil{}
每一个方法目标应该是单一的,大多数状况下每一个方法内都只有一个断言语句;方法不该该依赖于其余方法的结果做为输入,保证原子性。
实践3 断言语句须要解释测试者的意图。
解释 基本功
每种单元测试框架都提供了不少断言语句,从根本上来讲它们都是同样的。可是测试者须要根据本身的目的选择适当的语句,这样才可让别人阅读测试代码时理解用例设计的目的。例如对于STAssertNil和STAssertNotNil等等。
实践4 判断某个意图有没有达到的很好的方法是检测方法影响的数据有没有合理的变化。
解释 基本功
因为单元测试是使用断言语句来作判断的,于是最容易作的就是判断数据的变化。这也就限定了单元测试能测试的方法范围,即引发数据变化的方法。对于一些纯展现的方法,例如播放一段特效,这种方法是没法靠单元测试来进行约束的。测试数据的特性包括取值范围(int、float等),排列顺序(NSArray等),类型等等。
实践5 运用重构的手段使方法变得易于被测试。
解释 单元测试是保障重构安全的手段,重构也可使代码易于被测试。
什么样的代码是容易进行单元测试的?最简单的一点就是,每一个被测方法都应该是功能单一的。固然,这也是代码规范中应该作到的。方法的功能单一,则测试方法的断言也会比较好肯定。若是你发现某个方法很难进行测试,则就应该对这个方法进行拆分重构。
实践5.1 面向抽象设计类之间的关系。
解释 利于伪造类的实现。
类之间通信若是依赖于抽象(接口),则能够较容易的使用伪造类。参照实践1.1。
实践6 运用自上而下的方式构建类。
解释 自上而下的方式可使类的功能明确,类的构成将会清晰紧凑,不会出现一些废方法。
先肯定类须要负担的责任,以此来肯定类具备的公有方法以及属性。经过重构将公有方法中的代码转化为私有方法,以使方法尽可能短小紧凑。
实践6.1 应对全部暴露的属性和方法提供测试,私有方法则没必要。
解释 若是运用自上而下的方式构建类,则理论上私有方法应该都是公有方法重构而获得的。实际上测试公有方法时这些私有方法都应该被测试到了。并且,因为私有方法相对公有方法来讲发生变更的可能性很大,会形成没必要要的修改测试代码的成本。
回调方法不属于私有方法,也须要进行测试。
实践6.2 回调方法的测试方法是直接调用。
解释 基本功
因为回调方法通常是异步和不可触发的(按正常流程),例如网络事件的返回和按下按钮的触发事件。于是,测试的时候要直接调用来对其流程进行检测。例如某个按钮的touch up inside事件:
-(void)buttonPressed:(id)sender;
能够根据方法中用到的方法、属性伪造一个FakeButton按钮做为参数传递进行测试。
实践6.2 测试私有的方式,KVC、子类化和类别。
解释 基本功。
遇到须要经过验证私有数据才能编写的测试时,能够考虑使用KVC和子类化。子类继承于被测类,只包含于单元测试target,其做用就是在不应变受测类的状况下,使受测类具备某些易于被测的能力。
实践7:变化须要新测试的支持。
解释:保证测试的覆盖度。
就像敏捷中提到的“改变须要抽象”同样,在测试中改变须要新的测试。固然,度依然由程序员本身掌控。
4、通常流程
使用OCUnit最大的好处就是流程很是的简单,简单到让你以为很是愉悦。因为有XCode的支持,添加测试变得异常简单。只要在新建工程时勾选“Include Unit Tests”,就会自动的加入一个示例。而后再须要添加新的单元测试时,新建一个“Objective-C test case class”就能够了。
测试文件中,只要知道setUp是初始化的地方,tearDown是结束清理的地方,并且它们在每一个用例方法执行时都会从新执行--这保证了测试用例的原子性。而后知道每一个测试用例都是以test做为前缀的,而且无返回值。而后在方法中编写断言语句就能够了。输入STAssertxxxxx就能够看到它们的联想提示。编写完成后,执行菜单Product->Test,单元测试就完成了!
5、测试驱动(TDD)
敏捷当中提到了TDD这种开发方式。TDD的主旨是使开发者对其编写的代码更有信心,使开发者修改代码时内心更加踏实。对于其总结,仍是引用原文比较稳当:“测试驱动开发的妙处即在于,它以需求为引领,经过测试的形式,来指导开发者进行软件的设计与架构,并编写出最为精炼的代码,使得测试用例运行经过。通过适当的重构以后,测试用例与产品代码可达到较为健康的状态。”也就是上面提到的,经过自上而下的形式设计类,经过单元测试来不停地审视和重构类,从而达到代码的健康。
若是在代码写完以后在编写单元测试,那么就体现不出这种模式的好处了。这就好像写完代码再补文档同样,没有什么意义。测试应该在代码开始以前,或者在代码编写中不停地进行编写更新,这样才能使代码不停进步。这也正是TDD的意思。
6、总结
单元测试的代码如此简单,可是想写好单元测试却并非一件简单的事情。它须要程序员比较深的功底。因为我的水平所限,有一些东西说的比较啰嗦。把复杂问题简单化是本事,任重而道远。但愿你们能够在平常开发中运用好这种简洁高效的技术。