4.测试失败的调试ios
是时候追踪以前测试失败的问题了。打开GameBoard.m,找到cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 方法,你会看到它们都调用了一个叫作checkBoundsForColumn:andRow: 的helper方法,用来检测数组边界。数组
头文件 GameBoard.h 里的方法注释以下:app
// 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 < 0 || column > 7 || row < 0 || row > 7) [NSException raise:NSRangeException format:@"row or column out of bounds"]; }
从新运行测试,这时全部测试应该可以经过了。测试
自从代码不一样步,注释里的规范老是有一点危险。然而,你的测试能够为注释添加双保险。自从你编写测试代码后,只要你常常运行测试,这些实现不匹配的风险会大大减少!spa
另外,你的测试提供了一个伟大的高层概览代码,特别是遵循建议的命名规范之后。当你从新进入好久没碰过的代码后,这会很是方便——正以下一小节的内容。调试
5.保证测试bugcode
一个崩溃报告刚刚进入你的app:你的一个测试人员报告你,当她运行游戏后直接点击屏幕(不点击"2 Player"或者"vs computer"按钮),程序就会崩溃。
你本身试一遍,就会在控制台看到下列信息:
ReversiGame[1842:a0b] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'row or column out of bounds'
崩溃看起来重复发生,是什么抛出了一个NSRangeException ?call stack提供了如下信息:
2 CoreFoundation +[NSException raise:format:] + 139
3 ReversiGame -[GameBoard checkBoundsForColumn:andRow:] + 142 4 ReversiGame -[GameBoard cellStateAtColumn:andRow:] + 76
5 ReversiGame -[ReversiBoard flipOpponentCountersForColumn: andRow:withNavigationFunction:toState:] + 281
6 ReversiGame -[ReversiBoard makeMoveToColumn:andRow:] + 245 7 ReversiGame -[BoardSquare cellTapped:] + 192
从下往上读:
第七、6行 tap触发代码用来处理玩家的移动
第5行 游戏逻辑检查是否有对手的棋子被包围而且翻转
第四、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:3 andRow:3], 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:3 andRow:3]) 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不止是让你的代码更健康,同时让你有更多时间思考你的单元测试。
3、下一步何去何从?
测试是开发生涯的一个巨大任务,这章咱们掌握了单元测试的基础,下面是一些有益的概念:
Xcode中整合的XCTest让创建测试套件很是容易,整个iOS领域的测试范围是很是广大的,更多测试概念:
在深度学习测试以前,这里有几个挑战来让你掌握本章的概念。
4、挑战
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 若是异常被抛出,这些断言会失败