[iOS翻译]《iOS7 by Tutorials》系列:在Xcode 5里使用单元测试(下)
4.测试失败的调试
是时候追踪之前测试失败的问题了。打开GameBoard.m,找到cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 方法,你会看到它们都调用了一个叫做checkBoundsForColumn:andRow: 的helper方法,用来检测数组边界。
头文件 GameBoard.h 里的方法注释如下:
// raises an NSRangeException if the column or row are out of bounds
然而,如果超出边界,checkBoundsForColumn:andRow: 方法的实现抛出了一个NSGenericExpression 。通常的,你把头文件里的注释当做一个公共API规范,但在这个例子里代码和规范并不匹配,你该如何做?
一个可能性是更新注释和相关的测试,以匹配当前的实现。在这个例子里,规范里的注释看起来更有意义:一个边界检查应当遵循NSArray,并升起一个NSRangeException。
在GameBoard.m里,更新checkBoundsForColumn:andRow: 方法的实现如下:
- (void)checkBoundsForColumn:(NSInteger)column andRow:(NSInteger)row
{
if (column < || column > || row < || row > )
[NSException raise:NSRangeException
format:@"row or column out of bounds"];
}
重新运行测试,这时所有测试应该能够通过了。
自从代码不同步,注释里的规范总是有一点危险。然而,你的测试可以为注释添加双保险。自从你编写测试代码后,只要你经常运行测试,这些实现不匹配的风险会大大减小!
另外,你的测试提供了一个伟大的高层概览代码,特别是遵循建议的命名规范以后。当你重新进入很久没碰过的代码后,这会非常方便——正如下一小节的内容。
5.保证测试bug
一个崩溃报告刚刚进入你的app:你的一个测试人员报告你,当她运行游戏后直接点击屏幕(不点击"2 Player"或者"vs computer"按钮),程序就会崩溃。
你自己试一遍,就会在控制台看到下列信息:
ReversiGame[:a0b] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'row or column out of bounds'
崩溃看起来重复发生,是什么抛出了一个NSRangeException ?call stack提供了以下信息:
CoreFoundation +[NSException raise:format:] +
ReversiGame -[GameBoard checkBoundsForColumn:andRow:] + ReversiGame -[GameBoard cellStateAtColumn:andRow:] +
ReversiGame -[ReversiBoard flipOpponentCountersForColumn: andRow:withNavigationFunction:toState:] +
ReversiGame -[ReversiBoard makeMoveToColumn:andRow:] + ReversiGame -[BoardSquare cellTapped:] +
从下往上读:
第7、6行 tap触发代码用来处理玩家的移动
第5行 游戏逻辑检查是否有对手的棋子被包围并且翻转
第4、3行 代码然后调用cellStateAtColumn:andRow: 和
checkBoundsForColumn:andRow:
第2行 底层框架报出一个越界异常
想了解更多调试崩溃的信息?看这里
My App Crashed, Now What?
http://www.raywenderlich.com/10209/my-app-crashed-now-what-part-1
Demystifying iOS Application Crash Logs
http://www.raywenderlich.com/23704/demystifying-ios-application-crash-logs
这是一个测试这些崩溃条件的绝好机会。
你的新测试不止要修复这个问题,而且要作为一个回归测试确保这个bug保持修复。没有什么比修复一个bug后,数月之后添加新功能或重构代码时再遇到相同的bug更让人不爽的了。
6.决定什么需要测试
你知道你需要测试——但应该测试什么?
ReversiBoard 是GameBoard类的通用实现,所以从这里开始故障排除工作是有意义的。
使用iOS\Cocoa Touch\Objective-C test case class 模板创建一个新类,命名为ReversiBoardTests, 继承自XCtestCase。
在开始之前,删除模板文件的testExample方法,然后在ReversiBoardsTests.m 里导入头文件:
#import "ReversiBoard.h"
在ReversiBoardsTests.m 里改变@interface 如下:
@interface ReversiBoardTests : XCTestCase { ReversiBoard *_reversiBoard;
}
添加一个_reversiBoard 实例变量意味着你不用在每个测试方法里反复实例化。
然后修改setUp方法如下:
- (void)setUp {
[super setUp];
_reversiBoard = [[ReversiBoard alloc] init];
}
7.测试否定
在之前的编写的测试中,异常的存在是预期的结果。这一次,异常并没有在你的测试基础上出现。
添加这些方法到ReversiBoardsTests.m

- (void)test_makeMove_inPreGameState_nothingHappens {
[_reversiBoard setToPreGameState];
XCTAssertNoThrowSpecificNamed(
[_reversiBoard makeMoveToColumn: andRow:],
NSException,
NSRangeException,
@"Making a move in the pre-game state should do nothing");
}
上面的代码中,测试设置游戏前的状态。也就是说,玩家作出对战AI还是对战其他玩家选择之前的状态。这个测试模拟了一进入游戏就点击棋盘的动作。
XCTAssertNoThrowSpecificNamed 断言与XCTAssertThrowsSpecificNamed 刚好相反。如果指定的异常被抛出,上面的测试会失败;如果指定的异常没被抛出,测试会通过。
你还没有修复bug,所以现在运行代码将会失败——不过这是件好事,在修复bug之前编写测试意味着你拥有重现bug的测试能力。
Command+U 运行测试,你会看到如下信息:
test failure: -[ReversiBoardTests test_makeMove_inPreGameState_nothingHappens] failed: (([_reversiBoard makeMoveToColumn: andRow:]) does not throw <NSException, "NSRangeException">) failed
8.校正代码
打开 ReversiBoard.m 然后找到 makeMoveToColumn:andRow: 方法。
思考一下如何修正这个特定的bug。只有用户选择了游戏模式之后才可以移动,这是很有意义的。这样一想,游戏前和游戏后的游戏逻辑没有什么不同。
幸运的是,这里有一个属性指定当前的游戏状态:gameState.
添加以下代码到makeMoveToColumn:andRow: 方法的顶部:
if ([self gameState] != GameStateOn) return;
这个条件检测了当前的游戏状态。如果状态不是GameStateOn——说明游戏不在运行中——方法立即终止。
运行app,测试一下在选择游戏模式之前点击棋盘,是否崩溃?
最后,Command+U 运行测试,Test Navigator应该显示绿色小勾,bug被碾碎了!
探索风格的测试只用包含明显问题的代码,然而回归风格的测试则可以为经常修复某个问题提供了保障。
修复每个bug不止是让你的代码更健康,同时让你有更多时间思考你的单元测试。
三、下一步何去何从?
测试是开发生涯的一个巨大任务,这章我们掌握了单元测试的基础,下面是一些有益的概念:
- 使用哪一个断言?在哪里使用断言?
- 添加测试到一个现有app
- 在程序说明里使用测试
- 探寻并修复bug
- 确保已经修复过的bug不再出现
Xcode中整合的XCTest让建立测试套件非常容易,整个iOS领域的测试范围是非常广大的,更多测试概念:
- Mock objects
- 在测试里模拟出足够真实的对象
- http://ocmock.org/
- 在测试里模拟出足够真实的对象
- UI testing
- 可以模拟出用户的输入,比如touch或文本输入。
- Continuous integration (CI) systems
- 将会自动运行单元测试,想了解更多关于CI的功能,阅读本书14、15章“Beginning and Intermediate Continuous Integration in Xcode 5
在深度学习测试之前,这里有几个挑战来让你掌握本章的概念。
四、挑战
GameBoard 类仍然还有一些方法没被测试——你的任务是编写测试,为你的app提供一个完整的测试套件。
1.挑战一:测试 clearBoard
clearBoard 清除棋盘上的所有棋子。自从已经测试getter和setter 方法后,你可以假设这些方法无需再次测试。
celarBoard的测试用例有以下几个步骤:
1)至少设置一个黑棋在棋盘上
2)至少设置一个白棋在棋盘上
3)调用clearBoard
4)检查你现在放置白棋和黑棋的地方是空的(提示:状态为BoardCellStateEmpty)
记住测试用例遵循的命名格式:工作单元或方法名、测试什么、预期的结果
2.挑战二:测试scorekeeper
countCellsWithState: 记录棋盘上特定状态棋子的数量。这个方法计算出最后的分数,所以确保它正确工作是非常必要的!
countCellsWithState: 的测试用例将执行以下动作:
1)设置一些黑棋或白棋在棋盘上
2)追踪棋子增加的数量
3)比较你的数量与countCellsWithState:返回的数量
countCellsWithState:有一个状态参数,所以它看起来像这样
[_board countCellsWithState:BoardCellStateWhitePiece]
再次,确定你的测试用例命名恰当
祝你测试成功!
附录:XCTest断言参考
下列所有断言都使用(format...)作为最后一个参数,这个NSlog风格的参数会在测试失败时显示消息。
XCTFail(format...)
无条件失败;用来标记不应执行的代码部分 XCTAssertNil(exp, format...)
XCTAssertNotNil(exp, format...)
表达式应为nil或not nil;在OC对象中使用 XCTAssert(exp, format...)
XCTAssertTrue(exp, format...)
XCTAssertFalse(exp, format...)
表达式应为true或false XCTAssertEqualObjects(a1, a2, format...)
OC对象a1和a2应该相等;使用isEqual: 来保持相等 XCTAssertEqual(a1, a2, format...)
参数a1和a2应该相等;用来比较C数量、集合及结构体(如CGRect和CGPoint);使用NSValue来比较 XCTAssertEqualWithAccuracy(a1, a2, delta, format...)
参数a1与参数a2应该与给定的delta值相等;使用float和double类型,其中小数值可能不够精确 XCTAssertThrows(exp, format...)
XCTAssertThrowsSpecific(exp, exception, format...)
XCTAssertThrowsSpecificNamed(exp,exception,exceptionName,format...)
表达式应该抛出一个异常信息;更详细的版本让你指出类名和异常名 XCTAssertNoThrow
XCTAssertNoThrowSpecific
XCTAssertNoThrowSpecificNamed
如果异常被抛出,这些断言会失败
[iOS翻译]《iOS7 by Tutorials》系列:在Xcode 5里使用单元测试(下)的更多相关文章
- [iOS翻译]《iOS7 by Tutorials》在Xcode 5里使用单元測试(上)
简单介绍: 单元測试是软件开发的一个重要方面.毕竟,单元測试能够帮你找到bug和崩溃原因,而程序崩溃是Apple在审查时拒绝app上架的首要原因. 单元測试不是万能的,但Apple把它作为开发工具包的 ...
- [iOS翻译]《iOS7 by Tutorials》系列:在Xcode 5里使用单元测试(上)
简介: 单元测试是软件开发的一个重要方面.毕竟,单元测试可以帮你找到bug和崩溃原因,而程序崩溃是Apple在审查时拒绝app上架的首要原因. 单元测试不是万能的,但Apple把它作为开发工具包的一部 ...
- [iOS翻译]《iOS7 by Tutorials》系列:iOS7的设计精髓(下)
我们继续上篇的内容 四.聚焦于内容 在iOS7里,强调的不是眼花缭乱的装饰效果,而是最重要的内容本身. 下面我们来探讨这个主题: 1.删除不必要的内容 伟大的设计更多是减法和加法的组合. 虽然很酷的想 ...
- [iOS翻译]《iOS7 by Tutorials》系列:iOS7的设计精髓(上)
简介: 本文翻译自<iOS7 by Tutorials>一书的第一章“Designing for iOS 7”,主要从程序员角度介绍了iOS7的新设计理念,堪称神作!本文翻译仅作学习交流之 ...
- 【转】《iOS7 by Tutorials》系列:iOS7的设计精髓(下)
四.聚焦于内容 在iOS7里,强调的不是眼花缭乱的装饰效果,而是最重要的内容本身. 下面我们来探讨这个主题: 1.删除不必要的内容 伟大的设计更多是减法和加法的组合. 虽然很酷的想法是很重要,但还有更 ...
- 【转】《iOS7 by Tutorials》系列:iOS7的设计精髓(上)
简介: 本文翻译自<iOS7 by Tutorials>一书的第一章“Designing for iOS 7”,主要从程序员角度介绍了iOS7的新设计理念,堪称神作!本文翻译仅作学习交流之 ...
- [iOS翻译]《iOS 7 Programming Pushing the Limits》系列:你可能不知道的Objective-C技巧
简介: 如果你阅读这本书,你可能已经牢牢掌握iOS开发的基础,但这里有一些小特点和实践是许多开发者并不熟悉的,甚至有数年经验的开发者也是.在这一章里,你会学到一些很重要的开发技巧,但这仍远远不够,你还 ...
- 【疯狂造轮子-iOS】JSON转Model系列之二
[疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...
- 【疯狂造轮子-iOS】JSON转Model系列之一
[疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...
随机推荐
- .NET下dropdownlist的基本操作
//List列中索引的赋值 teacher.DataValueField = ds.Tables[0].Columns["pidcord"].ColumnName; //List列 ...
- OC中的复合
#import <Foundation/Foundation.h> #import "Car.h" int main(int argc, const char * ar ...
- 将spring源码导入到eclipse中
前置条件: 1. 正确安装jdk,并配置好JAVA_HOME.PATH.(我这里安装的是jdk1.8) 2. 正确安装好eclipse.(我的eclipse版本是: Neon Release (4.6 ...
- iOS网络-04-大文件下载
大文件下载注意事项 若不对下载的文件进行转存,会造成内存消耗急剧升高,甚至耗尽内存资源,造成程序终止. 在文件下载过程中通常会出现中途停止的状况,若不做处理,就要重新开始下载,浪费流量. 大文件下载的 ...
- 短信SMS的接收
近日,看了<第一行代码>有关短信接收的内容,就总结了一下. 1.手机接收到一条短信时,系统会发出一条android.provider.Telephy.SMS_RECEIVER的广播,这条广 ...
- I/O多路复用——epoll函数
1 select的低效率 select/poll函数效率比较低,主要有以下两个原因: (1)调用select函数后需要对所有文件描述符进行循环查找 (2)每次调用select函数时都需要向该函数传递监 ...
- hyper-v无线网络上外网
这个通过无线网络上外网也是找了很多文章,大部分写的都不详细,没有办法成功,原理就是创建一个虚拟网卡,然后把创建的虚拟网卡和无线网卡桥接,虚拟机中使用创建的虚拟网卡,这里创建的虚拟网卡指的是用hyper ...
- Linux awk
一.简介 二.教程 1)过滤字符(对大小写很敏感) dir -l | awk '$3=="root" {print $1,$3,$4, $9;} ' cat tecmint_dea ...
- myeclipse中运行tomcat报错java.lang.NoClassDefFoundError
有关myeclipse的小问题,在myeclipse中运行tomcat时显示已启动,但是无法访问localhost:8080/,显示404错误.在控制台中发现报错代码如下: java.lang.NoC ...
- MySql目录没有data文件夹怎么办
下载的是mysql的压缩包,解压后,更改my.ini文件,里面有个指向data文件夹的路径,但是mysql安装目录没有data文件夹,需要执行 mysqld --initialize --user=m ...