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


    简介:

    单元测试是软件开发的一个重要方面。毕竟,单元测试可以帮你找到bug和崩溃原因,而程序崩溃是Apple在审查时拒绝app上架的首要原因。

    单元测试不是万能的,但Apple把它作为开发工具包的一部分,不仅让你创作的APP更稳定,而且提供了一致、有趣的用户体验,这些都是让用户给你五星评价的源泉!iOS7提供了一个升级的单元测试框架,让你在Xcode中运行单元测试更为容易。当你完成这一章节,你将学会如何给现有app添加测试——并有可能培养出对编写测试的热爱!

    /*

    本文翻译自《iOS7 by Tutorials》一书的第十一章“Unit Testing in Xcode 5”,想体会原文精髓的朋友请到Raywenderlich商店支持正版。

    ——————(博客园、新浪微博)葛布林大帝

    */

    目录:

    一、单元测试基础

    二、开始项目

    三、下一步何去何从?

    四、挑战

    附录:XCTest断言参考

    一、单元测试基础

    在过去,Xcode引入了一个叫做OCUnit的开源单元测试框架。而在Xcode 5里面,Apple发布了他们自己的的单元测试框架,叫做XCTest。

    如果你已经熟悉OCUnit,别担心,XCTest是一个建立在OCUnit之上并且十份相似的API。从OCUnit过渡到XCTest非常简单,要做的仅仅是把STFail替换为XCTFail、STAssert替换为XCTAssert等等诸如此类。如果你已经熟悉这些基础,可以直接跳到下一节。

    1.高层次概述

     单元测试有四个层级。从上到下,它们分别是:

    • 测试套件(Test suite)
      • 这是项目里测试的全部集合。在Xcode里,测试套件被设置为一个单独的build target。
    • 测试用例类(Test case classes)
      • 正如你可能所期待的,在一个面向对象体系中,测试被整合到类里。在你的app里,每个测试类通常对应一个单独类。例如,DeveloperTests类应该对应Developer类的测试。
    • 测试用例方法(Test case methods)
      • 每个测试类包含多种方法,用来测试类的各种功能。就像一个方法或函数应该既精简又实用,每个测试用例应该测试一个特定的结果——并且完全测试。
    • 断言(Assertions)
      • 断言检查对应预期结果的具体条件。如果条件不符合预期结果, Xcode会报错指出断言失败。例如,可以断言你的Developer 类响应“writeKillerApp: message”;如果它没有,断言失败,Xcode报错。

    理论很美好,但有时举例会更容易阐述事物。用Empty Application 模板创建一个新项目,命名为EmptyApp。 Xcode模板会自动包含一个叫做EmptyAppTests的test target,添加到EmptyApp的 app target里,如下图:

    注意测试用例类包含了一个没有关联头文件的.m文件,打开EmptyAppTests.m看看第一个测试用例的源代码。

    测试方法必须以单词test开始,以便test runner能找到它们。在你的示例项目里,测试类包含了一个测试方法,叫做testExample

    setUptearDown方法就像守护在测试用例周围的卫兵一样。

    把所有对象的程序设置代码或重复性代码放到setUp里,使测试用例方法保持清爽、高效。

    类似的,关闭文件句柄或取消挂起网络请求等清理活动的方法应该放到tearDown里。

    Test runner 会依次调用setUp、testExample和tearDown方法。如果你申明了第二个测试方法testSecondExample,Test runner会依次调用setUp、testSecondExample,最后是tearDown方法。如果你有多个测试方法,setUp和tearDown会在一个测试环节调用多次——每经过一个测试用例方法调用一次!

    这个故事的寓意是不要放任何处理太慢或处理频繁的东西到setUp或tearDown方法里——这会让你运行测试套件时面临漫长的等待

    2.创建你的第一个测试

    testExample方法只有一个叫做XCTFail的语句,正如它名字里暗示的:总是会失败。这个语句不是非常有用,你可以写一个比它更好的!删除testExample方法,并添加如下方法:

    - (void)test_addition_twoPlusTwo_isFour 
    {   XCTAssert(
    2 + 2 == 4, @"2 + 2 should be 4 but %d was returned instead", 2+2); }

    测试用例的一个常用命名标准是:unitOfWork_stateUnderTest_expectedBehavior (工作单元_测试状态_预期行为)。

    在这个例子里,被测试的工作单元是加法,测试状态是2 + 2,预期行为是结果为4。

    所有XCTest断言都有前缀XCT。XCTAssert是可用于单元测试的简单断言,第一个参数是预评估为ture的表达式,当断言失败时,其后NSLog风格的参数会显示一条消息。

    确保项目的当前target为iPhone模拟器,通过窗口顶部目录的Product -> Test(Command-U)来运行测试,模拟器会启动并执行测试套件。如果通知处于激活状态,你会看到下列确认消息:

    为了证实第一个单元测试成功,切换到Test Navigator,箭头指出了它:

    哈哈!翠绿色的小勾旁边显示出了你的单元测试。

    你还可以看到边框空白处菱形图标旁的代码,如下所示:

    这些图标展示关联测试代码的状态:

    @implementation旁的绿色小勾表示这个类测试通过,test_addition_twoPlusTwo_isFour旁的绿色小勾表示这个方法测试通过。

    同时,这些图标也是按钮:

    点击@implementation旁的图标将会运行这个类的所有测试,点击其他测试方法旁的图标则会运行该测试方法,试一试吧!

     

    现在你已经对测试的概念和执行有了初步了解,是时候开始本章的示例项目了——测试开始!

    二、开始项目

    本章的剩余部分你将使用一个名为Reversi的黑白棋游戏项目,规则:两个玩家,分别代表白方和黑方,轮流在8x8棋盘上落子。通过包围对方棋子来吃掉它,游戏结束时棋子最多的为胜者。

    如何创建这个游戏,请看:
    http://www.raywenderlich.com/29228/how-to-develop-an-ipad-board-game-app-part-12

    下载本文页尾提供的示例项目并运行,点击屏幕下方的Vs Computer按钮与电脑进行对战,感受一下这个游戏的界面和玩法。

    你获胜了吗?或者被AI对手爆出翔?不管怎样,你的工作不是整日玩游戏——是时候添加一些有用的测试到项目里了。

     

    1.添加测试的支持

    第一个需要单元测试的是GameBoard类。这个类囊括了8x8棋盘的基本逻辑,64个单元格中的每个都有一个状态——空、黑棋或白棋——并且GameBoard实例让你能获取并设置每一个方块的状态。 

    打开GameBoard.h看一下里面的方法,在开始为现有代码编写测试之前,弄清楚各方法的作用和实现是一个好主意。

    GameBoard.h,你会看到下列两个方法:

    // gets the state of the cell at the given location
    // raises an NSRangeException if the column or row are out of bounds
    - (BoardCellState) cellStateAtColumn:(NSInteger)column andRow:(NSInteger)row;
    // sets the state of the cell at the given location
    // raises an NSRangeException if the column or row are out of bounds
    - (void) setCellState:(BoardCellState)state forColumn:(NSInteger)column andRow:(NSInteger)row;

    cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 由你非常熟悉的getter/setter模式里发展出来,你的第一个测试是执行如下动作:

    • 初始化一个GameBoard实例
    • 设置cell状态
    • 获取cell状态
    • 从指定的cell里获取cell状态

    第一步是创建一个GameBoard测试类,右击ReversiGameTests分组,选择 iOSCocoa TouchObjective-C test case class 创建一个名为GameBoardTests的测试类,继承自XCTestCase。

    确保你的新测试用例添加到ReversiGameTests target,如下图(这个步骤非常重要,如果没添加到正确的target里,你的测试不会运行):

     

    打开 GameBoardTests.m 并且删除 testExample 方法,你不需要它。

    然后在 GameBoardTests.m 顶部导入头文件(这仅仅是让你的测试类能够访问GameBoard类):GameBoard.h 

    #import "GameBoard.h"

    你需要为你的所有测试提供一个GameBoard 实例,创建一个实例变量会比在每个测试里申明一个清爽得多。

    GameBoardTests.m 里更新@interface 如下:

    @interface GameBoardTests : XCTestCase 
    {

      GameBoard *_board; }

    现在你有了_board实例变量,可以开始测试了。

    setUp 方法是第一次初始化_board的好地方,修改setUp如下:

    - (void)setUp
    {
      [super setUp];
      _board = [[GameBoard alloc] init]; 
    }

    现在这个类的所有测试用例方法都能够访问初始化后的_board实例变量了。

     

    2.第一个测试

    这是你需要为首个测试用例添加的所有步骤,添加以下方法到GameBoardTests.m

    - (void)test_setAndGetCellState_setValidCell_cellStateChanged 
    {
      [_board setCellState:BoardCellStateWhitePiece forColumn:4 andRow:5];
      BoardCellState retrievedState = [_board cellStateAtColumn:4 andRow:5];
      

      XCTAssertEqual(BoardCellStateWhitePiece,            retrievedState,            @"The cell should be white!"); }

    上面的代码在(4,5)单元格里设置了一个白棋,并且立刻检索了相同单元格的状态。XCTAssertEqual 断言检查它们是否相等,如果不相等,你会看到一个异常信息,然后你将得知有一些东西需要检查。

    上面代码的方法名遵循我之前提到的格式,通过这个方法名,你可以很容易看出它通过设置正确的单元格位置来测试setter和getter方法,并期待单元格状态的改变。

     

    如果你的测试工作是有计划的,确保iPhone和iPad模拟器都测试,然后运行测试(Command-U)。

    切换到Test Navigator,你会看到一个绿色小勾表示测试通过,如下图:

    这看起来只是一个简单的测试,但是它在调试错误里提供了巨大的价值。

     

    在内部, GAMEBOARD类使用一个简单的二维数组来跟踪8X8棋盘。但如果你曾经改变了代表向量或矩阵的数组,本次测试将作为回归测试,确保interface 的基础仍在工作。

    作为一个附带的好处,为现有的类编写测试可以大大有助于理解代码是如何工作的。分析类的方法可以帮助你辨别其功能,并为你编写测试提供便利。

     

    3.测试异常

    按照设计的功能测试代码有助于确保其正确性,但也使得你的app“早早失败或高调失败”——那些异常游戏状态或无效条件被调试器很快抓住。

    GameBoard.h里cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 方法的注释表明,如果行或列超出棋盘边框,它们会弹出错误。看起来你已经找到更多的测试条件。

    添加下列两个方法:

    - (void)test_setCellState_withInvalidCoords_exceptionThrown {
    XCTAssertThrowsSpecificNamed(
    [_board setCellState:BoardCellStateBlackPiece
    forColumn:10
    andRow:7], NSException,
    NSRangeException,
    @"Out-of-bounds board set should raise an exception");
    }
    - (void)test_getCellState_withInvalidCoords_exceptionThrown {
    XCTAssertThrowsSpecificNamed(
    [_board cellStateAtColumn:7 andRow:-10],
    NSException,
    NSRangeException,
    @"Out-of-bounds board access should raise an exception");
    }

    上面的代码里,test_setCellState_withInvalidCoords_exceptionThrown: 试图设置超出范围的单元格(10,7),同时test_setCellState_withInvalidCoords_exceptionThrown: 试图获取超出范围的单元格(7,-10)。再次的,方法名已指出在正测试不正确的坐标,报出异常正在意料之中。

     

    XCTAssertThrowsSpecificNamed 采用以下四点作为参数:

    • 应该报出异常的表达式
    • 排除的类
    • 排除的名称
    • 测试失败时显示的消息

    点击Command-U运行测试,你应该看到以下结果

    这是什么?你希望用出色的代码通过测试,但是两个错误标记在Issue Navigator上。测试失败信息也会显示在代码上,如下图:

    所有的测试失败消息为:

    [GameBoardTests test_getCellState_withInvalidCoords_exceptionThrown] failed: (([_board cellStateAtColumn:7 andRow:-10]) throws <NSException, "NSRangeException">) failed: throwing <NSException, "NSGenericException", "row or column out of bounds"> - Out-of- bounds board access should raise an exception

    如果你分解上面的消息,你会看到你希望的行为是(throws <NSException, "NSRangeException">) ,而实际发生的是(throwing <NSException, "NSGenericException">) 。

    在这个例子里,你期待的是NSRangeException ,但接收到的却是NSGenericException 。

    看起来你已经做了一些研究!

    示例项目地址:
    
    http://pan.baidu.com/s/1o6x6zxg

     

  • 相关阅读:
    Maven常用仓库地址以及手动添加jar包到仓库
    将Jar安装到本地仓库和Jar上传到私服
    maven release插件将一版本发布到仓库中时Return code is: 401, ReasonPhrase:Unauthorized
    使用github作为maven仓库
    关闭 将jar或者aar发布到到mvn 中(用github作为仓库), 通过gradle dependency 方式集成
    使用Spring进行统一日志管理 + 统一异常管理
    Error pulling origin: error: The following untracked working tree files would be overwritten by...
    c语言函数---M
    C++面试题一大波
    kettle中调用java类
  • 原文地址:https://www.cnblogs.com/yangfaxian/p/3782856.html
Copyright © 2020-2023  润新知