开始之前

本文侧重讲述如何在iOS程序的开发过程中使用单元测试。使用Xcode自带的OCUnit作为测试框架。

一、单元测试概述

单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代码的健康性。其目标是证明软件能够正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,但是很显然这与降低软件开发成本这一目的背道而驰)。它是对软件质量的一种保证,例如重构之后我们需要保证软件产品的正常运行。

很多人认为编写单元测试没有用是认为单元测试并不能保证一定能减少bug发生的几率,而由于编写单元测试一定会花费一定的时间与精力,因而必然的会增加成本。客观的说,造成这种原因很大的程度上是程序员的水平不够高。我认为使用使用单元测试带来巨大好处的必要条件如下所示:

  • 程序员本身的编程水平--是否有较多的代码经验,是否熟练掌握重构
  • 程序员对项目的认知--是否能正确理解软件或模块的需求
  • 项目质量--是否稳定,是否长期多版本,是否需要应对较多变化

如果程序员水平较高,对需求理解较为清晰,项目需要面对较多的变化,那么毫无疑问单元测试对于软件非常有益。假如软件功能简单且开发周期短,不需要进行复杂的维护工作,那么单元测试的意义并不大。

优秀的单元测试实践的好处:

  • 好的单元测试就是一份好的文档,并且比文档更能为程序员所接受,它直接描述了测试员对受测代码的结果所持的预期。
  • 当代码由别人维护时(或自己进行重构时),通过单元测试的约束,才能保证在加入新功能或修改旧功能时代码的正确性。
  • 由于单元测试的自动化执行,保证了在整个开发流程中代码都会被测试,这非常符合XP思想。
  • 保证在面对软件功能的变化时,程序员可以较为放心的进行代码重构,而不必担心是否破坏了原有功能。
  • 好的单元测试可以降低bug数量,而对于项目管理来说,修改bug这个过程是无法制定计划的,可以使软件的开发流程更容易掌控。
  • 可以由老程序员编写描述某个类行为的测试,以此指导新程序员对类的编码。
  • ……

好处还有很多,但最重要的一点就是保证了软件质量的同时,由于减少bug和应对变化造成的回归bug的产生等,提高了劳动生产率。而且,在敏捷流程中,使用单元测试是必须掌握的手段,否则就没发保证重构的正确性,从而造成代码无法面对变化。

二、iOS的单元测试概述

刚接触客户端编程时,我在很长一段时间内都想不通对于客户端程序如何编写单元测试。单元测试本质上说白了就是用一些断言来判定结果,而这种方式是如何应用到具有复杂交互的界面测试上来的呢?

我们要做的就是将客户端代码转化为易于测试的代码。什么样的代码易于测试呢?它至少是这样的:

1、被测方法需要产生可测量的结果。

2、类之间的关系应该是松耦合的。

其中第一条是必要条件。使用断言这种形式指明了测试的方法最终要造成某些可以度量的结果。因而,我们需要尽量的将展示和业务逻辑分离开来。展示的代码是没法测试的,例如有的方法只是播放动画。而业务逻辑最终都会造成一些数据的改变,这是容易测试的。

大略的讲,作为一个iOS程序员来说,首先要了解一个叫做MVC的模式。这个模式定义了Cocoa Touch框架的总体结构。在iOS程序中,我们也需要按照这种模式进行界面代码的编写。这样设计出来的类具有较好的结构,且比较适合于做单元测试。

然后一定要懂得不停重构代码,这样我们才能使代码不停地改善,不停地变得更加适合单元测试。

有一些框架可以帮助大家更好的测试,分别是OCUnit、GTM、GHUnit、CATCH、OCMock,但目前对我来说,OCUnit足够用了。作为苹果官方提供的测试框架,它最大的优点就是简单易用。

 三、单元测试实践

下面是一些我所理解的单元测试中比较好的实践。

顾名思义,单元测试面向的对象是单元,这个专有名词源自编译器领域的术语“编译单元”。在面向过程中,指的是函数,而在面向对象中,指的通常就是“类”。因而,每个功能类都应该提供对应的单元测试。

实践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:变化需要新测试的支持。

解释:保证测试的覆盖度。

就像敏捷中提到的“改变需要抽象”一样,在测试中改变需要新的测试。当然,度依然由程序员自己掌控。

四、一般流程

使用OCUnit最大的好处就是流程非常的简单,简单到让你觉得非常愉悦。由于有XCode的支持,添加测试变得异常简单。只要在新建工程时勾选“Include Unit Tests”,就会自动的加入一个示例。然后再需要添加新的单元测试时,新建一个“Objective-C test case class”就可以了。

测试文件中,只要知道setUp是初始化的地方,tearDown是结束清理的地方,而且它们在每个用例方法执行时都会重新执行--这保证了测试用例的原子性。然后知道每个测试用例都是以test作为前缀的,并且无返回值。然后在方法中编写断言语句就可以了。输入STAssertxxxxx就可以看到它们的联想提示。编写完成后,执行菜单Product->Test,单元测试就完成了!

五、测试驱动(TDD)

敏捷当中提到了TDD这种开发方式。TDD的主旨是使开发者对其编写的代码更有信心,使开发者修改代码时心里更加踏实。对于其总结,还是引用原文比较妥当:“测试驱动开发的妙处即在于,它以需求为引领,通过测试的形式,来指导开发者进行软件的设计与架构,并编写出最为精炼的代码,使得测试用例运行通过。经过适当的重构之后,测试用例与产品代码可达到较为健康的状态。”也就是上面提到的,通过自上而下的形式设计类,通过单元测试来不停地审视和重构类,从而达到代码的健康。

如果在代码写完之后在编写单元测试,那么就体现不出这种模式的好处了。这就好像写完代码再补文档一样,没有什么意义。测试应该在代码开始之前,或者在代码编写中不停地进行编写更新,这样才能使代码不停进步。这也正是TDD的意思。

六、总结

单元测试的代码如此简单,但是想写好单元测试却并不是一件简单的事情。它需要程序员比较深的功底。由于个人水平所限,有一些东西说的比较啰嗦。把复杂问题简单化是本事,任重而道远。希望大家可以在日常开发中运用好这种简洁高效的技术。

最后,作为一个中国人,祈福雅安。网上有一些人在这种事件上还在喷。我想说的是,虽然这个世界是客观存在的,但是每个人的世界观确是主观的。一个人观察世界的角度决定了他认为这个世界是什么样的。中华民族的民族性格是内敛的,在外在表现上似乎不如西方人张扬。但是就像汶川一样,全国人民在这种时刻表现出了我们这个民族内在的优秀品质。老鼠屎在哪里都是老鼠屎,我以自己是中国人为荣。

原文地址:http://www.cnblogs.com/slegetank/archive/2013/04/20/3030875.html

iOS开发之单元测试的更多相关文章

  1. iOS开发:XCTest单元测试(附上一个单例的测试代码)

    测试驱动开发并不是一个很新鲜的概念了.在我最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结果是否正确.我所学习第一门语言是c语言,用的最多的是在算法设计上,那时候最常做的事情就是编 ...

  2. iOS开发中的单元测试(三)——URLManager中的测试用例解析

    本文转载至 http://www.cocoachina.com/cms/plus/view.php?aid=8088   此前,我们在<iOS开发中的单元测试(一)&(二)>中介绍 ...

  3. iOS开发的知名大牛博客小汇

    王巍的博客:王巍目前在日本横滨任职于LINE.工作内容主要进行Unity3D开发,8小时之外经常进行iOS/Mac开发.他的陈列柜中已有多款应用,其中番茄工作法工具非常棒.http://onevcat ...

  4. iOS开发学习概述及知识整理

    设计师设计出来了一个不错的引导界面,然而当我看到设计稿的时候,我们的app也没几天就要上线了.这个界面模仿了Evernote iOS app的风格. 我以迅雷不及掩耳盗铃之势开始在Xcode上编程,用 ...

  5. ios 开发中 动态库 与静态库的区别

    使用静态库的好处 1,模块化,分工合作 2,避免少量改动经常导致大量的重复编译连接 3,也可以重用,注意不是共享使用 动态库使用有如下好处: 1使用动态库,可以将最终可执行文件体积缩小 2使用动态库, ...

  6. Android与Swift iOS开发:语言与框架对比

    Swift是现在Apple主推的语言,2014年新推出的语言,比Scala等“新”语言还要年轻10岁.2015年秋已经开源.目前在linux上可用,最近已经支持Android NDK:在树莓派上有Sw ...

  7. iOS开发UI篇—常见的项目文件介绍

    iOS开发UI篇—常见的项目文件介绍 一.项目文件结构示意图 二.文件介绍 1.products文件夹:主要用于mac电脑开发的可执行文件,ios开发用不到这个文件 2.frameworks文件夹主要 ...

  8. iOS开发之静态库(四)—— 静态框架framework制作

    前面介绍过,虽然苹果公司禁止在iOS开发中使用动态库,而且也从XCode中移除了创建静态框架的功能,但我们还是可以通过对XCode进行小小的改动来创建静态框架. 编译环境:Macbook Air + ...

  9. 那些在学习iOS开发前就应该知道的事(part 2)

    英文原文:Things I wish I had known before starting iOS development—Part 2 http://www.cocoachina.com/ios/ ...

随机推荐

  1. 基于webapi的移动互联架构

    又到了一年最后一次上班了,写下这篇日志作为本年总结. 首先总体介绍一下项目背景,今年公司开发了一款app,本人一个人负责app的接口服务.微信开放平台搭建以及系统后台,上线半年,如今活跃用户数3W+. ...

  2. git: fatal: Not a git repository (or any of the parent directories): .git

    在看书 FlaskWeb开发:基于Python的Web应用开发实战 时,下载完源码后 git clone https://github.com/miguelgrinberg/flasky.git 试着 ...

  3. google tensorflow guide

    # For CPU-only version $ pip install https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow- ...

  4. 解决自定义Shiro.Realm扩展类不能用注解(@Resource或@Autowire)自动装配的问题

    问题产生原因:加载Realm时其他Spring配置文件(xml)尚未加载,导致注入失败. 解决方法:编写一个设置类把注入工作提前完成. package com.xkt.shiro import org ...

  5. jeecms3.0.4版本 详解请求如何找到首页(转)

    第一步:发送http://localhost:8080/emisstrade/ 请求 第二步:首先进入配置文件web.xml, <context-param> <param-name ...

  6. php json_encode

    1.该函数只接受utf-8编码的字符串,其他编码的字符串会返回false 2.字符串中的斜线'/'会被自动转义成'\/',如果不想被转义,可用如下方式(适用于php5.4及以上) json_encod ...

  7. quartz配置文件详解

    quartz配置文件详解(转载)     quartz学习总结: 一.关于job:    用Quartz的行话讲,作业是一个执行任务的简单Java类.任务可以是任何Java代码.只需你实现org.qu ...

  8. Linux面试知识点总结

    1.Linux关机重启命令: 在linux命令中reboot是重新启动,shutdown -r now是立即停止然后重新启动,都说他们两个是一样的,其实是有一定的区别的.    shutdown命令可 ...

  9. Ext.js的store里放model,还是field?

    按别人的经验, 一般来说,如果通用性强的应用,STORE里存放MODEL,便于重用代码. 如果通用性较弱的(报告,图表),则考虑使用field进行定制.

  10. Microsoft Visual Studio 开发的C++程序软件发布相关事宜

    VS2005/VS2008软件发布: Debug版本: 非MFC程序: 编译选项mdd: 仅依赖C++库和C运行时库,需要到VS2005/VS2008下安装目录VC/redist/Debug_NonR ...