在Objc.io #1的Testing View Controllers中讲解的就是单元测试的相关内容。本文说下如何通过Xcode 5中集成的XCTest框架进行简单的单元测试。

什么是单元测试

首先什么是单元测试?维基百科中的解释是:


计算机编程中,单元测试(又称为模块测试, Unit
Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个
程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。


常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书
(en:Specification)要求的工作目标,没有程序错误;虽然单元测试不是什么必须的,但也不坏,这牵涉到项目管理的政策决定。

由于我们OC程序员写的通常都是面向对象的程序,所以我们在进行单元测试时,通常都是以一个方法为单位测试,目的当然是保证其不会出错了。按照Objc.io文章的观点,如果我们代码之间的耦合度很小,那么可以将其分解成多个小部件,从而更易于测试。

原文还提到一个概念:TDD(Test-Driven
Development),即测试驱动开发,该模式要求开发者在编写某个功能的代码之前先将其测试代码写好,然后编写实现代码并进行测试,从而保证实现的
代码不会出现问题。因此整个项目的开发进度将由测试来驱动,这有助于开发出高质量而又正确的代码,实现敏捷开发。(Objc.io上面的文章真的非常的
好)

好吧,科普完了,下面进入Xcode Unit Testing的部分。

XCTest

1.第一个单元测试

XCTest是Xcode 5中自带的测试框架,它和Xcode 4及之前的SenTestKit,OCUnit有什么前因后果,小弟没有做多少研究,所以不说了。

下面从一个Demo开始。首先用Xcode新建一个工程UnitTestDemo,工程目录结构如下:

可以看到工程下面多了一个叫UnitTestDemoTests的部分,Targets也多了一个UnitTestDemoTests,根据图标初步认为该Target跑的是一个框架。

这两个多出来的东西(相比Xcode 4没有Include Unit Tests的工程)就是用来做单元测试的,其特点是文件名或Target名都以Tests结尾。

再来看下UnitTestDemoTests.m中的代码:

  1. #import <XCTest/XCTest.h>
  2. @interface UnitTestDemoTests : XCTestCase
  3. @end
  4. @implementation UnitTestDemoTests
  5. - (void)setUp
  6. {
  7. [super setUp];
  8. // Put setup code here. This method is called before the invocation of each test method in the class.
  9. }
  10. - (void)tearDown
  11. {
  12. // Put teardown code here. This method is called after the invocation of each test method in the class.
  13. [super tearDown];
  14. }
  15. - (void)testExample
  16. {
  17. XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__);
  18. }
  19. @end

该类继承自XCTestCase类,其中包含三个方法:setUp,tearDown和testExample。

setUp方法用于在测试前设置好要测试的方法,tearDown则是在测试后将设置好的要测试的方法拆卸掉。

testExample顾名思义就是一个示例。

按快捷键Command + U进行单元测试,结果如下:

可以看到没有通过测试,在Issue Navigator和控制台都输出了错误信息:本类中的testExample方法没有实现。

实际上,这个错误是我们主动抛出来的。XCTFail是一个宏,其作用就是让测试失败,后面的No implementation for \"%s\"", __PRETTY_FUNCTION__就是要报告的错误信息,由我们自定。

报错总是让人不爽,好吧,我们将其注释掉,另外写一个测试方法,尝点甜头。为了规范,建议每个测试方法都写成“ - (void)testXXX ”形式,XXX表示要测试的方法名,并且无返回类型。

  1. //- (void)testExample
  2. //{
  3. //    XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__);
  4. //}
  5. - (void)testTrue {
  6. XCTAssert(1, @"Can not be zero");
  7. }

Command + U,搞定:

注意左边的Test Navigator,绿色的标志表示测试全部通过。

2.测试的顺序

如果在同一测试类文件中多写几个方法,例如:

  1. - (void)testTrue2 {
  2. NSLog(@"2222222222222222222222");
  3. XCTAssert(1, @"Can not be zero");
  4. }
  5. - (void)testTrue1 {
  6. NSLog(@"1111111111111111111111");
  7. XCTAssert(1, @"Can not be zero");
  8. }
  9. - (void)testTrue3 {
  10. NSLog(@"3333333333333333333333");
  11. XCTAssert(1, @"Can not be zero");
  12. }
  13. - (void)testAtrue {
  14. NSLog(@"0000000000000000000000");
  15. XCTAssert(1, @"Can not be zero");
  16. }

控制台部分输出:

  1. Test Case '-[UnitTestDemoTests testAtrue]' started.
  2. 2014-03-19 21:19:38.182 UnitTestDemo[7401:60b] 0000000000000000000000
  3. Test Case '-[UnitTestDemoTests testAtrue]' passed (0.001 seconds).
  4. Test Case '-[UnitTestDemoTests testTrue1]' started.
  5. 2014-03-19 21:19:38.183 UnitTestDemo[7401:60b] 1111111111111111111111
  6. Test Case '-[UnitTestDemoTests testTrue1]' passed (0.000 seconds).
  7. Test Case '-[UnitTestDemoTests testTrue2]' started.
  8. 2014-03-19 21:19:38.184 UnitTestDemo[7401:60b] 2222222222222222222222
  9. Test Case '-[UnitTestDemoTests testTrue2]' passed (0.013 seconds).
  10. Test Case '-[UnitTestDemoTests testTrue3]' started.
  11. 2014-03-19 21:19:38.196 UnitTestDemo[7401:60b] 3333333333333333333333
  12. Test Case '-[UnitTestDemoTests testTrue3]' passed (0.001 seconds).

可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。

目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,小者优先,例如testA,testB1,testB2三个方法相继执行。

3.断言测试

下面一共18个断言(SDK中也是18个,其含义转自ios UnitTest 学习笔记,真心佩服原文的博主,部分宏小弟已经测试过):

XCTFail(format…) 生成一个失败的测试;

XCTAssertNil(a1, format...)为空判断,a1为空时通过,反之不通过;

XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过;

XCTAssert(expression, format...)当expression求值为TRUE时通过;

XCTAssertTrue(expression, format...)当expression求值为TRUE时通过;

XCTAssertFalse(expression, format...)当expression求值为False时通过;

XCTAssertEqualObjects(a1, a2, format...)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;

XCTAssertNotEqualObjects(a1, a2, format...)判断不等,[a1 isEqual:a2]值为False时通过;

XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);

XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);

XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;

XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;

XCTAssertThrows(expression,
format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
XCTAssertThrowsSpecific(expression, specificException, format...)
异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;

XCTAssertThrowsSpecificNamed(expression,
specificException, exception_name,
format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;

XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;

XCTAssertNoThrowSpecific(expression, specificException, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;

XCTAssertNoThrowSpecificNamed(expression,
specificException, exception_name,
format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

特别注意下XCTAssertEqualObjects和XCTAssertEqual。

XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。

XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。

对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES。例如下面代码中只有第二行可以通过测试:

  1. // 1.比较基本数据类型变量
  2. XCTAssertEqual(1, 2, @"a1 = a2 shoud be true"); // 无法通过测试
  3. XCTAssertEqual(1, 1, @"a1 = a2 shoud be true"); // 通过测试

但是,如果a1和a2都是指针,那么只有a1和a2指向同一个对象才会返回YES。例如下面的代码中:

  1. // 3.比较NSArray对象
  2. NSArray *array1 = @[@1];
  3. NSArray *array2 = @[@1];
  4. NSArray *array3 = array1;
  5. XCTAssertEqual(array1, array2, @"a1 and a2 should point to the same object"); // 无法通过测试
  6. XCTAssertEqual(array1, array3, @"a1 and a2 should point to the same object"); // 通过测试

array1和array2指向不同对象,无法通过测试。

这里比较奇怪的是,NSString另当别论:

  1. // 2.比较NSString对象
  2. NSString *str1 = @"1";
  3. NSString *str2 = @"1";
  4. NSString *str3 = str1;
  5. XCTAssertEqual(str1, str2, @"a1 and a2 should point to the same object"); // 通过测试
  6. XCTAssertEqual(str1, str3, @"a1 and a2 should point to the same object"); // 通过测试

尽管str1和str2指向不同的对象,但是二者的指针比较却能通过测试。不知道这是不是XCTest框架本身的一个Bug,反正在这里使用NSString要小心就是了。

由于str1和str2指向同一常量,常量在内存的data段中地址是固定的,所以二者地址相同。

掌握了各个断言的含义,用起来就没什么大问题了。

4.简单的应用

说了一大堆理论和定义,下面来点实际的应用。下面有一个表格控制器:

  1. #import "TableViewController.h"
  2. #import "TableDataSource.h"
  3. static NSString * const kCellIdentifier = @"Cell";
  4. @interface TableViewController ()
  5. @property (strong, nonatomic) TableDataSource *dataSource;
  6. @end
  7. @implementation TableViewController
  8. @synthesize dataSource = _dataSource;
  9. - (void)viewDidLoad
  10. {
  11. [super viewDidLoad];
  12. TableViewCellConfigureBlock cellConfigureBlock = ^(UITableViewCell *cell, NSString *item) {
  13. cell.textLabel.text = item;
  14. };
  15. NSArray *stringsArray = @[@"1", @"2", @"3", @"1", @"2", @"3", @"1", @"2", @"3", @"1", @"2", @"3", @"1", @"2", @"3", @"1", @"2", @"3", @"1", @"2", @"3", @"1", @"2", @"3"];
  16. self.dataSource = [[TableDataSource alloc] initWithItems:stringsArray
  17. CellIdentifier:kCellIdentifier
  18. ConfigureCellBlock:cellConfigureBlock];
  19. self.tableView.dataSource = _dataSource;
  20. }
  21. @end

该类的UITableViewDataSource委托由一个TableDataSource类实现(将UITableViewDataSource分离,见Objc.io #1Lighter View Controllers)。TableDataSource类的初始化方法如下:

  1. - (instancetype)initWithItems:(NSArray *)anItems
  2. CellIdentifier:(NSString *)aCellIdentifier
  3. ConfigureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock
  4. {
  5. self = [super init];
  6. if (self) {
  7. self.items              = anItems;
  8. self.cellIdentifier     = aCellIdentifier;
  9. self.configureCellBlock = [aConfigureCellBlock copy];
  10. }
  11. return self;
  12. }

下面写一个Tests类测试一下DataSource的初始化方法。首先新建一个test case class类:

继承自XCTestCase类:

为了规范,我们新建的测试类都应该以Tests结尾,例如CellConfigureTests。

然后写个testDataSourceInitializing方法:

  1. - (void)testDataSourceInitializing {
  2. TableViewCellConfigureBlock cellConfigureBlock = ^(UITableViewCell *cell, NSString *item) {
  3. cell.textLabel.text = item;
  4. };
  5. TableDataSource *tableSource = [[TableDataSource alloc] initWithItems:@[@"1", @"2", @"3"]
  6. CellIdentifier:@"TestCell"
  7. ConfigureCellBlock:cellConfigureBlock];
  8. XCTAssertNotNil(tableSource, @"TableView data source should not be nil");
  9. }

Command + U运行测试。如果TableDataSource初始化成功,那么tableSource将不会为nil,测试就能通过。

Demo下载地址:点此进入下载页

本文先说到这里,下一篇博客说下如何借助更加强大的OCMock和GHUnit进行单元测试。

参考资料:

iOS7初体验(2)——单元测试

ios UnitTest 学习笔记

XCode下的iOS单元测试

Lighter View Controllers

Testing View Controllers

单元测试

Xcode 5 单元测试(一)使用XCTest进行单元测试的更多相关文章

  1. xcode 5 使用 XCTest 做单元测试

    xcode 5 使用 XCTest 做单元测试 什么是单元测试,请看 百度百科 单元测试 一:在xcode5 之前,我们新建项目时,可以选择是否集成单元测试:如今在xcode5,我们新建立的项目默认就 ...

  2. 单元测试系列之三:JUnit单元测试规范

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6762032.html Junit测试代 ...

  3. <VS2017> 编写VC++单元测试 -(一)新建单元测试工程

    开发人员自己编写单元测试是一个非常好的习惯.单元测试不但能够验证自己所编写的代码是否存在问题,避免提交给测试人员时才发现bug,也可以为将来改动代码的人提供验证代码功能正确性的途径.在我有限的工作生涯 ...

  4. 如何通过Xcode 5中集成的XCTest框架进行简单的单元测试

    XCTest 1.第一个单元测试 XCTest是Xcode 5中自带的测试框架 下面从一个Demo开始.首先用Xcode新建一个工程UnitTestDemo,工程目录结构如下: 可以看到工程下面多了一 ...

  5. 单元测试或main方法 进行单元测试时 idea检查其他类的语法是否正确的去除方法

    在进行单元测试或者main方法时,在 运行/调试 设置中设置想要使用的测试单位的 before launch 即可

  6. 【maven】【spring boot】【单元测试】 使用controller 执行单元测试类

    存在这样一个场景: 当项目启动时间过长,又没办法缩短的时候,写单元测试就是一个十分耗时的工作, 这工作不在于使用编写代码,而在于每次run junit test 都需要完整启动一次项目,白白浪费宝贵的 ...

  7. .NET单元测试艺术(2) - 第一个单元测试

    List 2.1 使用[SetUp]和[TearDown]特性 using System; using System.Collections.Generic; using System.Linq; u ...

  8. 使用unittest单元测试框架对加法做单元测试

    import unittest from parameterized import parameterized def cacl(a, b): return a+b class MyCacl(unit ...

  9. 上传NUnit的单元测试结果和OpenCover的单元测试覆盖率到SonarQube服务中

    SonarQube.Scanner.MSBuild.exe begin /k:"OMDCCQuotes" /d:sonar.host.url="http://myip:9 ...

随机推荐

  1. sharePreference的几个重点

    一.  SharePreferences是用来存储一些简单配置信息的一种机制,使用Map数据结构来存储数据,以键值对的方式存储,采用了XML格式将数据存储到设备中,文件存放在/data/data/&l ...

  2. Android记事本开发02

    今天: 继续学习基础知识. 昨天: 学习了ADB工具的基本命令. Android项目的目录结构. AndroidManifest.xml Android应用程序的打包和安装 遇到的问题: 无.

  3. PHP的几种遍历方法

    PHP常用的遍历方法有三种,foreach,for,list()/each()和while,这三种方法中效率最高的是使用foreach语句遍历数组 一.使用for语句循环遍历数组 值得大家注意的是使用 ...

  4. 公共文件js加载

    头部:例如 <header id="header" class="clearfix"> <a class="col-xs-9&quo ...

  5. win2008服务器信任问题

    右键计算机,管理,在第一个页面里面有个安全信息,里面的右边有一个配置IE ESC ,点击他后会出现一个窗口,在那里面选择禁用即可!

  6. Nodejs express框架 浅析

    http://www.expressjs.com.cn/ 1. 中间件 ①挂载中间件的函数:app.use var http = require('http'); var express = requ ...

  7. 汕头市队赛 SRM 09 A 撕书

    A 撕书I-3 SRM 09 背景&&描述 琉璃在撕书.     书总共有n页,都悬浮在数轴上,第i页的位置为,上面写着一个数字.     琉璃从右往左撕书.假如看到了第i页,就把在第 ...

  8. 自定义View Draw过程(4)

    目录 目录 1. 知识基础 具体请看我写的另外一篇文章:自定义View基础 - 最易懂的自定义View原理系列 2. draw过程作用 绘制View视图 3. draw过程详解 同measure.la ...

  9. python--asyncio模块

    申明:本文章参考于https://www.jianshu.com/p/b5e347b3a17c 网络模型有很多种,为了实现高并发也有很多方案,多线程,多进程.无论多线程和多进程,IO的调度更多取决于系 ...

  10. This template requires a more recent version of the Android Eclipse plugin. Please update from versi

    新建android project的时候遇到这个错误: 解决方法:①直接修改F:\JAVA\SDK\android-sdk\tools\templates\activities (对应你的JAVA S ...