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里使用单元测试(下)的更多相关文章

  1. [iOS翻译]《iOS7 by Tutorials》在Xcode 5里使用单元測试(上)

    简单介绍: 单元測试是软件开发的一个重要方面.毕竟,单元測试能够帮你找到bug和崩溃原因,而程序崩溃是Apple在审查时拒绝app上架的首要原因. 单元測试不是万能的,但Apple把它作为开发工具包的 ...

  2. [iOS翻译]《iOS7 by Tutorials》系列:在Xcode 5里使用单元测试(上)

    简介: 单元测试是软件开发的一个重要方面.毕竟,单元测试可以帮你找到bug和崩溃原因,而程序崩溃是Apple在审查时拒绝app上架的首要原因. 单元测试不是万能的,但Apple把它作为开发工具包的一部 ...

  3. [iOS翻译]《iOS7 by Tutorials》系列:iOS7的设计精髓(下)

    我们继续上篇的内容 四.聚焦于内容 在iOS7里,强调的不是眼花缭乱的装饰效果,而是最重要的内容本身. 下面我们来探讨这个主题: 1.删除不必要的内容 伟大的设计更多是减法和加法的组合. 虽然很酷的想 ...

  4. [iOS翻译]《iOS7 by Tutorials》系列:iOS7的设计精髓(上)

    简介: 本文翻译自<iOS7 by Tutorials>一书的第一章“Designing for iOS 7”,主要从程序员角度介绍了iOS7的新设计理念,堪称神作!本文翻译仅作学习交流之 ...

  5. 【转】《iOS7 by Tutorials》系列:iOS7的设计精髓(下)

    四.聚焦于内容 在iOS7里,强调的不是眼花缭乱的装饰效果,而是最重要的内容本身. 下面我们来探讨这个主题: 1.删除不必要的内容 伟大的设计更多是减法和加法的组合. 虽然很酷的想法是很重要,但还有更 ...

  6. 【转】《iOS7 by Tutorials》系列:iOS7的设计精髓(上)

    简介: 本文翻译自<iOS7 by Tutorials>一书的第一章“Designing for iOS 7”,主要从程序员角度介绍了iOS7的新设计理念,堪称神作!本文翻译仅作学习交流之 ...

  7. [iOS翻译]《iOS 7 Programming Pushing the Limits》系列:你可能不知道的Objective-C技巧

    简介: 如果你阅读这本书,你可能已经牢牢掌握iOS开发的基础,但这里有一些小特点和实践是许多开发者并不熟悉的,甚至有数年经验的开发者也是.在这一章里,你会学到一些很重要的开发技巧,但这仍远远不够,你还 ...

  8. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  9. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

随机推荐

  1. CSS ID选择器(三)

    一.ID选择器 ID选择器使用"#"前缀标识符进行标识,后面紧跟指定的元素的ID名称. 如 #box{ width:100px; height:100px;} 元素的ID名称是唯一 ...

  2. iOS开发笔记2:单例模式(singleton)

    每一个app有且仅有一个UIApplication,类似UIApplication“ [UIApplication sharedApplication]”这种一个类有且仅有唯一实例的设计即单例模式. ...

  3. C语言-09-文件操作

    文件类型 文本文件(ASCII) 简单的文本文件,可用任何文字处理程序阅读 二进制文件 包含 在ASCII及扩展ASCII字符中编写的数据或程序指令 的文件,通常图形文件及文字处理程序等计算机程序都属 ...

  4. mysql中FIND_IN_SET的使用方法

    在mysql中,有时我们在做数据库查询时,需要得到某字段中包含某个值的记录,但是它也不是用like能解决的,使用like可能查到我们不想要的记录,它比like更精准,这时候mysql的FIND_IN_ ...

  5. Effective Java 62 Document all exceptions thrown by each method

    Principle Always declare checked exceptions individually, and document precisely the conditions unde ...

  6. 线程本地存储TLS(Thread Local Storage)的原理和实现——分类和原理

    原文链接地址:http://www.cppblog.com/Tim/archive/2012/07/04/181018.html 本文为线程本地存储TLS系列之分类和原理. 一.TLS简述和分类 我们 ...

  7. JavaScript的几种函数的结构形式

    匿名函数,普通函数,变量函数,基于对象的方法 介绍它们的优劣点(性能,执行条件,可维护性,适合大型还是小型) Javascript有着灵活多变的函数方法,具体选用何种形式,都会极大地影响应用程序的编写 ...

  8. uva 11134 fabled rooks (贪心)——yhx

    We would like to place n rooks, 1 n 5000, on a n nboard subject to the following restrictions• The i ...

  9. 该怎样提高ZBrush的创作效率

     ZBrush是一款数字雕刻和绘画软件,以强大的功能和直观的工作流程改变了整个三维行业,相信使用ZBrush的人都希望加快雕刻速度提高ZBrush技能,很多雕刻专家也都试图证明加快雕刻速度是否真的能提 ...

  10. 原创翻译-测试驱动开发(TDD)

    测试驱动开发原则 翻译自<<Expert Python Programming>> 测试驱动开发是指首先编写包含所有测试软件特点的测试集,然后再去开发软件.也就是说,在编写软件 ...