几周前,我决定将将我在 mokacoding 上的创作更多集中在单元测试与验收测试,自动化和生产效率上,主要在iOS领域。
相关深入文章可以看看“通过 CocoaPods 为 iOS 项目创建 Calabash 并构建配置”和“用终端运行 Xcode 测试”。
这周我们要回过头来看看,或者说是站在更高的角度审视单元测试和验收测试,以及在云端运行持续集成有哪些资源。
就像有人创建 walking skeleton 时会做的事情一样,我们也将先查看 Cocoa 和 Xcode 提供给开发者的工具,然后再看看能实现更好效果的开源库,最后整理出在云端持续集成环境运行测试的解决方案。
Xcode,测试开始的地方
伴随着 iOS 7 和 Xcode 5,苹果发布了 XCTest,一个简单而又强大的测试编写框架,使用了同 xUnit 一样的风格。
编写 XCTest 测试很简单,开发者在 Xcode 点击 ⌘U 运行测试便能持续不断地迅速获得反馈。
Xcode 还有一个叫“Test Navigator”的界面,它可以让我们看到所有测试点,包括在最后一次运行后的成功或失败状态。
值得注意的是,红色为测试失败,绿色为测试通过。在不断迭代过程中颜色会给你很大帮助。
XCTest 已经高度集成在 Xcode 中,使用简单方便。这是它主要优点,也是缺点。XCTAssert
类 API 并不容易理解,也不灵活。从 Xcode 外边运行测试也没有你想象的那么简单。
在过去两年中,iOS 和 OS X 的单元测试框架已经变得越来越好,而验收测试这边反而没什么进步。
苹果提供了 UIAutomation 框架来编写 UI 自动化测试。UIAutomation 测试使用 javascript 写成。允许用户使用代码驱动应用 UI 并给它的状态设置断言。尽管看上去很美好,使用 UIAutomation 其实是很繁琐的, javascript API 也没有原生代码写成的单元测试那样强大。
这是 UIAutomation 测试的一个小片段。
1
2
3
4
5
|
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的代替品,支持行为描述句式。实例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 行为描述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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 写的,非常适合用新的语言写测试组件。
1
2
3
4
5
6
7
8
9
10
11
|
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 标签值来视图查询与交互。
1
2
3
4
5
6
7
8
|
- (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比较不好的地方在于作者响应时间较慢。这不是批判,毕竟开源世界一切都是免费的,但我们都要赚钱糊口,可以理解作者用在这些项目上的时间是有限的。但是当整个框架的基础都非常难以使用,那么他的稳定性一定很低。
Subliminal
Subliminal 是一个类似 KIF 的 Objective-C 框架,集成了 XCTest。和 KIF 不同的是,SUbliminal 是写在 UIAutomation 上层,旨在为开发者隐藏它的复杂性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- (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 中。我创建示例使用的是 Vim 和 Rake。
我们书写 Cucumber 特性,执行每一步,然后使用命令行测试。为了它能够工作,需要在应用内嵌入一个 HTTP 服务器,用于查询和驱动 UI。
不用说,这可能是一个很大坑。
Cucumber/Calabash 测试代码差不多是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 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 好的地方在于它是一种陈述式的测试,管理层会喜欢如果他们会读到这些测试的话。而且它可以兼容两个平台。
另一方面,工具链并不是非常强大。测试运行相对较慢,需要在 Cucumer,Ruby,Objective 之间持续交换,消耗相当多的时间。
就像单元测试,开源库提供了不同的选择,用于改进你的工作流。唯一不同的是这些工具没那么成熟,社区没那么活跃。
持续集成平台
为我们的项目套上好的测试工具,其最后一步是拥有持续集成。在开发者机器上运行测试并不能保证代码不会出错,毕竟其他团队成员会对代码进行更改。有个人来不断运行测试会更加安全。
不用说,最好的 CI 是在云端进行。配置维护一套 Jenkins 需要大量的时间。
CI 的选择会更多。这里列出一些支持 iOS 项目的主要 CI 服务。
它们之间的区别主要是在价格,上手容易程度,以及如何配置。例如 Travis CI 使用 .travis.yml
文件定义所有的步骤,而 Bitrise 则图形界面,每个步骤都用 block 展示,并且这block可以被添加到进程。
上面这个列表可能并不全面,我可能落了一些。希望这个对于有兴趣写测试和 CI 的人是一个好的开始。