如何简单地为测试切换 App Delegate

原文链接=http://qualitycoding.org/app-delegate-for-tests/
作者=Jon Reid
原文日期=2015/03/17


测试驱动的开发最大好处是能够有快速反馈(译者:这是作者的另一篇文章,讲述了测试驱动的好处,有兴趣的可以看看)。所以,为了确保你的 TDD 效率,最好的方式就是尽可能快地获得反馈。

但是很多 iOS 开发者会在测试的时候使用生产环境(译者:应用开发中的不同阶段,一般分为开发环境 development,处于产品开发阶段;生产环境 production,即正式上线的环境,更详细的请参照 Development, testing, acceptance and production)的 app delegate。这是一个影响效率的问题。

你的常规 app delegate 在用于测试时是否跟龟速一样?

这里写图片描述

这是因为当你测试运行时,首先要启动你的应用——而这个过程可能做了很多事情,大量耗时的操作。而这些耗时的操作在测试的时候并不是我们所需要的。

我们应该如何避免这个问题?

测试流程

Apple 习惯将单元测试归为两类:应用测试和逻辑测试。这个区别是非常重要的,因为在以前,应用测试只能在设备上运行,除非你使用完全不同的第三方测试框架。

但是这个差异现在消失了,因为 Apple 允许我们在模拟器上运行应用测试。Apple 花了很多时间来更新文档,直到在他们最新的Xcode测试才更新了这部分说明,Apple 现在称之为 “app tests” 和 “library tests”。这就使事情简化为你是开发一个应用还是一个库。并且 Xcode 为你设置了一个测试用的 target ,这正是你所需要的。

如果我现在开发一个应用(或者一个需要运行应用的库),我总是会运行应用测试,所以我停止去试图区分这两种类型的测试。但是由于 Xcode 是在一个运行的应用的上下文环境下执行应用测试,测试流程就变成这样:

  1. 启动模拟器
  2. 在模拟器中,启动应用
  3. 将测试 bundle 注入运行的应用
  4. 运行测试

那么我们怎么才能加快这个流程呢?我们可以在第二步中做文章,让应用尽可能快地启动。

普通的 app delegate

在开发环境下,启动应用可能会关闭很多任务。Ole Begemann 在 Revisiting the App Launch Sequence on iOS中进行了详细的解释,但是根本上, UIApplicationMain() 最终会调用 app delegate去执行 application:didFinishLaunchingWithOptions: 。具体的流程一般取决于你的应用,但是很少会像下面这么做:

  1. 创建 Core Data。
  2. 配置根视图控制器
  3. 检测网络连通性
  4. 向服务器发送一个网络请求去取回最近的配置,例如应该在根视图中展示的东西。

因此在开始测试之前要做很多事情。难道不能使我们的测试不受干扰,如果我们想要的只是运行我们的测试程序?

让我们来解决这个问题,下面是具体方案。

改变 main 函数

让我们改变我们的 main 函数,如下所示:

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

我们现在想要去检查是否我们在运行测试代码。如果想要这么做的话,我们想要去使用一个不同的 app delegate。我们可以这么做:

最早的版本

1
2
3
4
5
6
7
8
9
10
11
12
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "TestingAppDelegate.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
BOOL isTesting = NSClassFromString(@"XCTestCase") != Nil;
Class appDelegateClass = isTesting ? [TestingAppDelegate class] : [AppDelegate class];
return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass));
}
}

从根本上来说,如果 XCTestCase 链接好了,我们就会使用 TestingAppDelegate。否则,我们退而使用生产环境的 app delegate。然后我们启动应用时可以选择我们想要的 app delegate。(注意:TestingAppDelegate 必须在生产环境的 target 中)

现在这些代码已经实现了来回切换。上述部分的实现从根本上和我原先的文章一致。因为有一段时间,根据评论中的建议,我将代码改为:

1
2
3
4
5
6
7
@autoreleasepool {
Class appDelegateClass = NSClassFromString(@”XYZTestingAppDelegate”);
if( appDelegateClass == nil ) {
appDelegateClass = [DOAAppDelegate class];
}
return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass));
}

但是在Xcode7上不能正常运行,所以我又改回原始版本。

如果你想在单元测试外部使用 XCTest 该怎么办,例如 UI 测试?为了取代为 XCTestCase 做的测试,你可以设置一个环境变量,通过 getenv 来测试。

提供 TestingAppDelegate

这里需要创建一个 TestingAppDelegate 类。正如下面代码所示:

TestingAppDelegate.h

1
2
3
4
5
#import <UIKit/UIKit.h>
@interface TestingAppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *window;
@end

TestingAppDelegate.m

1
2
3
4
#import "TestingAppDelegate.h"
@implementation TestingAppDelegate
@end

正如你所看到的那样,不要做任何事。

(在早先的 iOS 版本中,我必须添加更多的代码,导致 TestingAppDelegate 会创建一个 window,给这个 window 设置一个不做任何事情的根视图,然后让其可见。现在看来没必要了。)

快速反馈的本质

最重要的事情是我们已经从本质上减少了测试过程中启动应用的步骤。尽管还有一些不必要的开销,但是并不多。这是实现快速反馈过程中重要的一步,这样我们就可以从 TDD 中获得更多。

甚至当你开始一个新的项目,我推荐尽早使用这样的方法,因为你真正的app delegate最终会变得日益庞大。让我们在襁褓中阻止这种问题,然后保持快速的反馈。

另外一个好处是,通过完全控制哪部分该测试,什么时候测试,我们现在可以编写跟生产环境的app delegate完全不同的单元测试。这显然是双赢的。