2015年 iOS 测试现状

文章翻译自 mokacoding,感谢 dopcn 校稿,收获颇多。

几周前,我决定将我在 mokacoding 上的创作更多集中在单元测试与验收测试,自动化和生产效率上,主要在iOS领域.

相关深入文章可以看看“通过 CocoaPods 为 iOS 项目创建 Calabash 并构建配置”和“用终端运行 Xcode 测试”。

这周我们要回过头来看看,或者说是站在更高的角度审视单元测试和验收测试,以及在云端运行持续集成有哪些资源。

就像有人创建 walking skeleton 时会做的事情一样,我们也将先查看 Cocoa 和 Xcode 提供给开发者的工具,然后再看看能实现更好效果的开源库,最后整理出在云端持续集成环境运行测试的解决方案。

Xcode,测试开始的地方

伴随着 iOS 7 和 Xcode 5,苹果发布了 XCTest,一个简单而又强大的测试编写框架,使用了同 xUnit 一样的风格。

编写 XCTest 测试很简单,开发者在 Xcode 点击 ⌘U 运行测试便能持续不断地迅速获得反馈。

Xcode 还有一个叫“Test Navigator”的界面,它可以让我们看到所有测试点,包括在最后一次运行后的成功或失败状态。

2015-04-17-Test-Navigator

值得注意的是,红色为测试失败,绿色为测试通过。在不断迭代过程中颜色会给你很大帮助。

XCTest 已经高度集成在 Xcode,使用简单方便。这个是它主要优点,也是缺点。XCTAssert API 不容易理解,难以适应变化,并且从 Xcode 外边运行测试没那么方便。

过去两年,iOS 和 OS X 的单元测试框架已经变得越来越好,而验收测试这边反而没什么进步。

苹果提供了 UIAutomation 框架来编写 UI 自动化测试。UIAutomation 测试使用 javascript 写成,它允许用户使用代码驱动 app UI 并给它的状态设置断言。尽管看上去很美好,但使用 UIAutomation 框架确实冗长乏味, javascript API 也没有原生的单元测试那么强大。

这是 UIAutomation 测试的一个小片段。

UIATarget.localTarget().frontMostApp().navigationBar().buttons()["Add"].tap();

UIATarget.localTarget().frontMostApp().mainWindow().tableViews()[0].cells()[0].elements()["Chocolate Cake"];

UIATarget.localTarget().frontMostApp().mainWindow().tableViews()[0].scrollToElementWithPredicate("name beginswith 'Turtle Pie'");

就像你所看到的,javascript API 比 Foundation 更加冗长。而且还有一点,这个测试需要从 Instruments 中运行,你就可以想象使用这个框架是多么的不爽。

最后是苹果的 CI 解决方案:Xcode Bots。我们可以配置一个 Xcode Bot,在需要的时候触发他工作,例如运行我们的测试,Xcode Bots 可以存放在服务器端。

我承认我自己并没有用过 Xcode Bots,但是我获得的所有反馈都告诉我这个东西并不好用。

总结下,如今缺乏好奇心的开发者和大公司,可以只使用苹果的技术,组建一整套运行在CI的单元测试和验收测试。用于工作基本上是足够了。

如果你正在阅读本文,你可能充满了好奇心,那么让我们继续看看开源社区有那些资源。

单元测试框架的开源库

iOS 和 OS X 开源社区充满了各种大牛和有趣的项目。在写本文的时候,在pod上一共有 8625 个开源项目。

这些单元测试的开源库主要都是行为描述风格(xSpec),一定程度上也反映了测试风格的一种趋势,这风格来自于 Ruby 测试库的 RSpec, 主要是测试类的行为,而不是枚举方法。

Kiwi

Kiwi 是一个全栈式的,XCTest的代替品,支持行为描述句式。实例代码如下

describe(@"Team", ^{
context(@"when newly created", ^{
it(@"has a name", ^{
id team = [Team team];
[[team.name should] equal:@"Black Hawks"];
});

it(@"has 11 players", ^{
id team = [Team team];
[[[team should] have:11] players];
});
});
});

Kiwi 行为描述使得我们更容易阅读和理解代码所想要测试的内容,它就像一个好的说明文档。

Kiwi 集成了一些测试方法 期望(expectations), 模拟对象 (mock),桩程序 (stub),甚至还支持异步测试

Specta

Specter 跟 Kiwi 有点像,但是它使用了不同的架构。Kiwi 是庞大的代替品 ,Specta 优势则体现在模块化与组件化。这个库关心的唯一事情是编写和运行 xSpec 风格的测试,然后用户可以根据使用期望(expectations), 匹配(matching),模拟对象(mock)和桩程序(stub)的情况来补充相应模块。

我个人更喜欢这个库的设计,轻量级,包含的多个模块可以被结合在一起。

这是 Specta 行为描述

SpecBegin(Thing)

describe(@"Thing", ^{
it(@"should do stuff", ^{
// This is an example block. Place your assertions here.
});

it(@"should do some stuff asynchronously", ^{
waitUntil(^(DoneCallback done) {
// Async example blocks need to invoke done() callback.
done();
});
});
});

注意 it 执行的时候 blocks 是空的。留给库的使用者来用他们喜欢的工具填写。
说道工具,这里有个库名单,他们都可以与 Specta 和 Kiwi 配合使用:

  • Expecta a matcher framework, expect(foo).to.equal(bar).
  • OCHamcrest another matcher framework, assertThat(foo, equalTo(bar)).
  • OCMock a mocking framework.
  • OCMockito another mocking framework.
  • OHTTPStubs a library to stub network requests, with block based syntax to match URLs.
  • Nocilla another library to stub network requests, with a nice chain-able API, stubRequest(@”POST”, ).withHeaders(…).withBody(…).

Quick

Quick 是一个新的测试框架,但实际上非常酷。主要代码都是用 Swift 写的,非常适合用新的语言写测试组件。

import Quick

class ThingSpec: QuickSpec {
override func spec() {
describe("a 'Thing'") {
it("should do stuff) {
//
}
}
}
}

多亏了 Swift 的语法和闭包,Quick 行为描述看起来比 Kiwi 和 Specta 的可读性更强。
Qucik 来自 Nimble 的一个 matcher 库,它允许用户编写简洁的测试,比如 expect(10) > 2

无论是 Objective-C 还是 Switf,单个庞大框架或是你喜欢的库组成的组件,开源社区提供了大量有价值的测试框架,特别是专注于写简洁测试的,感谢表达句法(expressive syntax)的存在。

验收测试的开源库

苹果提供的官方工具中单元测试框架和验收测试框架的质量对比也反应在开源社区中。可能是因为 XCTest 为开源测试框架提供了坚实的基础,而 UIAutomation 没有,所以我们只能选择一些非常规的方法.

KIF

KIF,保持函数式(Keep It Functional),这是一个用 Objective-C 写的框架,让我们使用 XCTest 编写验收测试,然后在 Xcode 运行,方式和我们在单元测试做的一样。

KIF 使用私有的 API 来获得视图层级,让我们可以使用 accessibility 标签值来视图查询与交互。

- (void)testSuccessfulLogin {
[tester enterText:@"user@example.com" intoViewWithAccessibilityLabel:@"Login User Name"];
[tester enterText:@"thisismypassword" intoViewWithAccessibilityLabel:@"Login Password"];
[tester tapViewWithAccessibilityLabel:@"Log In"];

// Verify that the login succeeded
[tester waitForTappableViewWithAccessibilityLabel:@"Welcome"];
}

KIF比较不好的地方在于作者响应时间较慢。这不是批判,毕竟开源世界一切都是免费的,我们并不需要为此买单,可以理解作者用在这些项目上的时间是有限的。但是当这个框架的基础功能都非常难以使用,那么它的稳定性一定很低。

2015/06/04 更新:在这篇文章发表的几个月后,KIF 的维护响应效率有着非常大的提升,所以之前的言论已经不太准确。现在 KIF 不仅是一个非常好的 UI 自动化测试和验收测试框架,而且有着非常活跃的社区。随着新版本的到来,将带来新的架构和性能的提升。KIF 的未来很有看头。

Subliminal

Subliminal 是一个类似 KIF 的 Objective 框架,集成了 XCTest。和 KIF 不同的是,SUbliminal 是写在 UIAutomation 上层,旨在对开发者隐藏它的复杂性。

- (void)testLogInSucceedsWithUsernameAndPassword {
SLTextField *usernameField = [SLTextField elementWithAccessibilityLabel:@"username field"];
SLTextField *passwordField = [SLTextField elementWithAccessibilityLabel:@"password field" isSecure:YES];
SLElement *submitButton = [SLElement elementWithAccessibilityLabel:@"Submit"];
SLElement *loginSpinner = [SLElement elementWithAccessibilityLabel:@"Logging in..."];

NSString *username = @"Jeff", *password = @"foo";
[usernameField setText:username];
[passwordField setText:password];

[submitButton tap];

// wait for the login spinner to disappear
SLAssertTrueWithTimeout([loginSpinner isInvalidOrInvisible], 3.0, @"Log-in was not successful.");

NSString *successMessage = [NSString stringWithFormat:@"Hello, %@!", username];
SLAssertTrue([[SLElement elementWithAccessibilityLabel:successMessage] isValid],
@"Log-in did not succeed.");

// Check the internal state of the app.
SLAssertTrue(SLAskAppYesNo(isUserLoggedIn), @"User is not logged in.")
}

Subliminal 声明它可以测试应用内购警告,甚至能使 app 进入睡眠。这听起来很牛,但事实是,在我写本文的时候,该库最近的一次代码提交是 2014年9月,而且还有 13 个活跃的 pull request,这都是不好的信号。

Calabash

目前我们所看到的工具,Calabash 是最早的一个。它是一个 Ruby 包,使用 Cucumber 编写 BDD 风格的验收测试,现在被 Xamarin 维护。Xamarin 是一个可以用 C# 写 iOS 和 Android 应用的框架。语言会不会有点多!

不像 KIF 和 Subliminal,Calabash 是完全不集成在 Xcode 的。我创建示例使用的时 VimRake

我们书写 Cucumber 特性,执行每一步,然后使用命令行测试。为了它能够工作,必须在应用内嵌入一个 HTTP 服务,用于查询和驱动 UI。

不用说,这可能是一个很大坑。

Cucumber/Calabash 测试代码差不多是这样:

# rating_a_stand.feature

Feature: Rating a stand
Scenario: Find and rate a stand from the list
Given I am on the foodstand list
Then I should see a "rating" button
And I should not see "Dixie Burger & Gumbo Soup"

# steps.rb

Given(/^I am on the foodstand list$/) do
wait_for_element_exists "view marked:'Foodstand'"
end

Given(/^I should see a "([^\"]*)" button$/) do |button_title|
wait_for_element_exists "button marked:'#{button_title}'"
end

Given(/^I should not see "([^\"]*)"$/) do |view_label|
wait_for_element_does_not_exists "view marked:'#{view_label}'
end

Calabash 积极的地方在于这是很陈述式的测试。写起来差不多是这样 if they’ll ever read them,而且它可以跨越连个平台。

另一方面,工具链并不是非常强大。测试运行较慢,需要在 Cucumer,Ruby,objective-C 之间持续交换,消耗相当多的时间。

就像单元测试,开源库提供了提供了各种不同的选择,可以改进你的工作流。唯一不同的是它工具没那么成熟,社区没那么活跃。

持续集成平台

为我们的项目套上好的测试工具,其最后一步是拥有持续集成。在开发者机器上运行测试并不能保证代码不会出错,毕竟其他团队成员会对代码进行更改。有个人来不断运行测试会更加安全。

不用说,最好的 CI 配置就放在云端。配置维护一套 Jenkins 需要大量的时间。

CI 的选择会更多。这里是一系列主要的 CI 服务,它们支持 iOS 项目。

它们之间的区别主要是在价格,上手容易程度,以及如何配置。例如 Travis CI 使用 .travis.yml 文件定义所有的步骤,而 Bitrise 则是图形界面,每个步骤都用 block 展示,并且这block可以被添加到进程。

上面这个列表可能并不全面,我可能落了一些。希望这个对于有兴趣写测试和 CI 的人是一个好的开始。