热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

iOS开发进阶之单元测试

iOS开发进阶之单元测试开始之前本文侧重讲述如何在iOS程序的开发过程中使用单元测试。使用Xcode自带的OCUnit作为测试框架。一、单元测试概述单元测试作为敏捷开发实践的组成之一,其目的是提高软件

iOS开发进阶之单元测试

开始之前

本文侧重讲述如何在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的意思。

六、总结

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

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

 
 
 
标签: TDD, 单元测试

推荐阅读
  • 本文介绍了在Mac上安装Xamarin并使用Windows上的VS开发iOS app的方法,包括所需的安装环境和软件,以及使用Xamarin.iOS进行开发的步骤。通过这种方法,即使没有Mac或者安装苹果系统,程序员们也能轻松开发iOS app。 ... [详细]
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文介绍了一种解析GRE报文长度的方法,通过分析GRE报文头中的标志位来计算报文长度。具体实现步骤包括获取GRE报文头指针、提取标志位、计算报文长度等。该方法可以帮助用户准确地获取GRE报文的长度信息。 ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • 本文介绍了Sencha Touch的学习使用心得,主要包括搭建项目框架的过程。作者强调了使用MVC模式的重要性,并提供了一个干净的引用示例。文章还介绍了Index.html页面的作用,以及如何通过链接样式表来改变全局风格。 ... [详细]
  • 本文介绍了2015年九月八日的js学习总结及相关知识点,包括参考书《javaScript Dom编程的艺术》、js简史、Dom、DHTML、解释型程序设计和编译型程序设计等内容。同时还提到了最佳实践是将标签放到HTML文档的最后,并且对语句和注释的使用进行了说明。 ... [详细]
  • 本文介绍了iOS开发中检测和解决内存泄漏的方法,包括静态分析、使用instruments检查内存泄漏以及代码测试等。同时还介绍了最能挣钱的行业,包括互联网行业、娱乐行业、教育行业、智能行业和老年服务行业,并提供了选行业的技巧。 ... [详细]
author-avatar
-生命之水-
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有