我们不是信奉测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。
  让事情保持简单
  测试简单的事情很简单,同样,测试复杂的会很复杂。像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动开发(简称 TDD),有些人喜欢它,有些人则不喜欢。我们在这里不深入讨论,只是如果用 TDD,你得在写代码之前先写好测试。如果有什么疑问,可以去看看 Wikipedia 上的文章。同时,我们也认为重构和测试可以很好地结合在一起。
  测试 UI 部分通常很麻烦,因为它们包含太多活动部件。通常,view controller 需要和大量的 model 和 view 类交互。为了使 view controller 便于测试,我们要让任务尽量分离。
  幸好,我们在更轻量的 view controller 这篇文章中的阐述的技术可以让测试更加简单。通常,如果你发现有些地方很难做测试,这说明你的设计出了问题,你应该重构它。你可以重新参考更轻量的 view controller 这篇文章来获得一些帮助。总的目标是有清晰的关注点分离。每个类只做一件事,并且做好。这样可以让你只测试这件事。
  记住:测试越多,回报的增长趋势越慢。首先你应该做简单的测试。当你觉得满意时,再加入更多复杂的测试。
  Mocking
  当你把一个整体拆分成小零件(即更小的类)时,我们可以在每个类中进行测试。但由于我们测试的类会和其他类交互,这里我们用一个所谓的 mock 或 stub 来绕开它。把 mock 对象看成是一个占位符,我们测试的类会跟这个占位符交互,而不是真正的那个对象。这样,我们可以针对性地测试,并且保证不依赖于应用程序的其他部分。
  在示例程序中,我们有个包含数组的 data source 需要测试。这个 data source 会在某个时候从 table view 中取出(dequeue)一个 cell。在测试过程中,还没有 table view,但是我们传递一个 mock table view,这样即使没有 table view,也可以测试 data source,像下面你即将看到的。起初可能有点难以理解,多看几次后,你能体会到它的强大和简单。
  Objective-C 中有个用来 mocking 的强大工具叫做 OCMock。它是一个非常成熟的项目,充分利用了 Objective-C 运行时强大的能力和灵活性。它使用了一些很酷的技巧,让通过 mock 对象来测试变得更加有趣。
  本文后面有 data source 测试的例子,它更加详细地展示了这些技术如何工作在一起。
  SenTestKit
  我们将要使用的另一个工具是一个测试框架,开发者工具的一部分:Sente 的 SenTestingKit。这个上古神器从 1997 年起伴随在 Objective-C 开发者左右,比第一款 iPhone 发布还早 10 年。现在,它已经集成到 Xcode 中了。SenTestingKit 会运行你的测试。通过 SenTestingKit,你将测试组织在类中。你需要给每一个你想测试的类创建一个测试类,类名以 Testing 结尾,它反应了这个类是干什么的。
  这些测试类的方法会做具体的测试工作。方法名必须以 test 开头来作为触发一个测试运行的条件。还有特殊的 -setUp 和 -tearDown 方法,你可以重载它们来设置各个测试。记住,你的测试类是个类而已:只要对你有帮助,随便在里面加 properties 和辅助方法。
  做测试时,为测试类创建基类是个不错的模式。把通用的逻辑放到基类里面,可以让测试更简单和集中。可以通过示例程序中的例子来看看这样带来的好处。我们没有使用 Xcode 的测试模板,为了让事情简单有效,我们只创建了单独的 .m 文件。通过把类名改成以 Tests 结尾,类名可以反映出我们在对什么做测试。
  与 Xcode 集成
  测试会被 build 成一个 bundle,其中包含一个动态库和你选择的资源文件。如果你要测试某些资源文件,你得把它们加到测试的 target 中,Xcode 会将它们打包到一个 bundle 中。接着你可以通过 NSBundle 来定位这些资源文件,示例项目实现了一个 -URLForResource:withExtension: 方法来方便的使用它。
  Xcode 中的每个 scheme 定义了相应的测试 bundle 是哪个。通过 ?-R 运行程序,?-U 运行测试。
  测试的运行依附于程序的运行,当程序运行时,测试 bundle 将被注入(injected)。测试时,你可能不想让你的程序做太多的事,那样会对测试造成干扰。可以把下面的代码加到 app delegate 中:
static BOOL isRunningTests(void) __attribute__((const));  - (BOOL)application:(UIApplication *)application         didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {     if (isRunningTests()) {         return YES;     }      //     // Normal logic goes here     //      return YES; }  static BOOL isRunningTests(void) {     NSDictionary* environment = [[NSProcessInfo processInfo] environment];     NSString* injectBundle = environment[@"XCInjectBundle"];     return [[injectBundle pathExtension] isEqualToString:@"octest"]; }
  编辑 Scheme 给了你极大的灵活性。你可以在测试之前或之后运行脚本,也可以有多个测试 bundle。这对大型项目来说很有用。重要的是,可以打开或关闭个别测试,这对调试测试非常有用,只是要记得把它们重新全部打开。
  还要记住你可以为测试代码下断点,当测试执行时,调试器会在断点处停下来。