随着测试在软件开发周期中越来越受到重视,BAT大部分开始取消了测试工程师职位,全部变成了测试开发职位。需要在有测试能力的基础上兼备开发能力;另一方面自动化测试成为趋势,利用开发的技巧解决测试中的问题以提高测试效率,降低QA与RD的人力比。
苹果官方测试工具
单元测试的开源库
验收测试的开源库
自动化测试平台
持续集成平台
1. XCTest
① XCTest是苹果在 iOS7 和Xcode 5引入的一个简单而强大的测试框架,它的测试编写起来非常简单,并且遵循xUnit
风格。XCTest 的优点是与 Xcode 深度集成,有专门的Test 导航栏,但因为受限于官方测试API,因此功能不是很丰富。
② XCTestCase:
如果项目创建的时间勾选了UniTest
(从名字上看就是Apple提供的官方的一个单元测试工具),我们可以看到工程里面是多了一个目录,默认多了一个目录,默认多了一个类,如图:
XCTest 是Apple 官方提供的一个测试工具,一个内置的测试框架,从工程里面可以看到,一个“应用名称”的group,我们直接可以使用command + R
来运行,一个测试的target我们可以使用command + U
来远行测试target, 在测试target的目录下会有一个默认的“应用名称”+Test的类,这个类只有 .m没有 .h,集成自XCTestCase
,使用command + U
即可运行。
默认测试类里面有以下方法:
// 方法在XCTestCase的测试方法调用之前调用,可以在测试之前创建在test case方法中需要用到的一些对象等
- (void)setup;
// 当测试全部结束之后调用tearDown方法,目的是全部的test case执行结束以后清理测试现场,释放资源删除不用的对象等
- (void)tearDown;
// 测试代码执行性能
- (void)testPerformanceExample;
③ XCTestCase使用:
XCTestCase的初始化不是用户控制的,开发者无需手动针对XCTestCase的subclass进行alloc、init或者调用静态方法初始化的操作,针对一个功能模块的单元测试(针对某个class),只需要单独给这个类创建一个继承于XCTestCase,在这个文件内实现上述基本函数以后(一半系统会默认创建者三个函数),其实的逻辑只需要开发者自行定义以”test”开头的函数,然后在内部实现自己针对某个函数、返回数值结果、操作等的测试脚本即可,command + U
执行的时间,单元测试会自动执行这些“test”打头的函数,当函数头上出现蓝色的标记则表明通过测试,否则直接报红色错误。
④XCTest测试范畴
- 基本逻辑测试处理测试
- 异步加载数据测试
- 数据mock测试
⑤XCTest常用基本测试工具
XCTest常用的一些判断工具都是以”XCT“开头的,如:
//断言,最基本的测试,如果expression为true则通过,否则打印后面格式化字符串
XCTAssert(expression, format...)
//Bool测试:
XCTAssertTrue(expression, format...)
XCTAssertFalse(expression, format...)
//相等测试
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...)
//double float 对比数据测试使用
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
//Nil测试,XCTAssert[Not]Nil断言判断给定的表达式值是否为nil
XCTAssertNil(expression, format...)
XCTAssertNotNil(expression, format...)
//失败断言
XCTFail(format...)
⑥XCTest异步测试
Xcode单元测试中加入的最令人兴奋的功能也许就是类XCTestExpression 类带入的异步测试了。现在测试可以等到指定长度的时间,一直到某些条件符合的时候开始测试。而不用再写很多的GCD代码控制
要使用异步测试,首先用方法experctationWithDescription创建一个expection
let expectation = expectationWithDescription("...")
之后,在方法的最后添加方法waitForExpectationsWithTimeout
,指定等待超时的时间和指定时间内条件无法满足时执行的closure。
waitForExpectationsWithTimeout(10) { (error) in
// ...
}
剩下的就是在异步测试剩下的回调函数中告诉expectation条件已经满足。
expectation.fulfill()
如果在测试中有多个expectation,则每个expectation都必须fulfill,否则测试不通过。
- (void)testFetcRequestWithMockedManagedObjectContext {
MockNSManagedObjectContext *mockContext = [[MockNSManagedObjectContext alloc] initWithConcurrentType: 0x00];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName: @"User"];
fetchRequest.predicate = [NSPredicate predicateWithFormat: @"email ENDSWITH[cd] apple.com"];
fetchRequest.resultType = NSDictionaryResultType;
NSError *error = nil;
NSArray *results = [mockContext executeFetchRequest: fetchRequest error: &error];
XCTAssertNil(error, @"error应该为nil");
XCTAssertEqual(results.count, 2, @"fetch request应该只返回一个结构");
NSDictionary *result = results[0];
XCTAssertEqual(result[@"name"], @"张三", @"name应该是张三");
NSLog(@"email: %@",result[@"email"]);
XCTAssertEqual(result[@"email"], @"[email protected]", @"email应该是[email protected]");
}
⑦ Code Converage工具使用
首先需要在product->scheme->Edit Scheme里面将Code Coverage模式打开,选中为debuge模式,如图:
打开Code Coverage模式之后,打开某个测试类,command + U
运行,如果测试通过,测试脚本的函数头上会出现一个绿色的标志(相反如何哪一个方法测试没有通过,则会提示一个红色错误),如下:
打开Xcode左边窗口的Report Navigator,找到 Project Log,选择最近一次的log选项,最近一次是刚才的一个Test Log,选中这个Log实例,可以看到一下界面,如果:
然后再tab中选中 Coverage, 此时你可以看到大致的代码执行覆盖情况,如果指示条是满的则代表该类代码全部跑过一遍。
双击你想要查看的类,此处选择查看 UATrackDao,打开后既可以看到刚刚的测试中有哪些代码是执行过的,那些代码时未执行的,橘黄色的代表还未执行的,执行过的每一行后面会有一个序号代表这行代码在刚才的测试过程中执行的次数。如果有未执行的,可以根据具体的情况调整对应的测试脚本,继续测试,最终确保每一行代码都能正确执行,如图:
2. UITest
①UnitTest简介
任何软件开发中,自动化UI测试都很重要的(UI自动化测试的好处此处就不再多说了),iOS平台在以往是通过UIAutomation来完成自动化UI测试的,它的测试用例是javascript写的(Instruments中提供了该功能),这个过程深奥繁琐,需要自行编写对应的测试脚本,速度慢,学习成本高(关于Automation自动化测试概念大家可以查看相关的资料,Automation自动化测试在各大平台都有应用,在大型的软件开发测试过程的确可以节省大量的手工测试人员,大大提高软件测试的成本与效率)。
Apple在Xcode 6中又新增了UnitTest之外,到了Xcode 7 Apple从新提供了一个新的框架 UITest,这个主要是用来测试UI的,UniTest涌来测试功能逻辑代码,UITest专门涌来测试UI。
Xcode 7已经集成了UITest,UITest允许你找到UI元素并与之交互,还能检查UI的属性和状态,并且UITest也已经集成在Xcode 的测试报告,可以和单元测试一起执行,和UnitTest一样我们可以在检查UI的时间执行断言。
创建UITest target,同样会生成一个“项目名称” + UITest的group, UITest target 可以在创建工程的时间勾选,也可以在工程中手动添加,在“项目名称” + UITest分组下,我们可以看到系统会帮助我们默认生成一个UI测试类,这个类同样是继承于XCTestCase的。由此可见,在iOS中无论是单元测试还是UI测试,都是基于XCTestCase的。
②UnitTest UI测试
创建模态视图,我们选择从第一个VC通过点击按钮的形式push到第二个VC
创建UITest target,我们队上述UI进行测试,如图选项:
打开UITrackDemoUITest.m,创建 -(void)testUI,同时将光标留在函数内
点击下面的红色汗牛,开始recorder操作,程序运行起来后,点击界面上的安妮,程序会push到一个新的界面,这个时间会看到刚才的鼠标光标处自动生成一部分代码,重复操作,每次都会生成新的代码,如图:
从新点击小红点按钮,此时结束recorder操作,commond + U
运行测试,此时刚才的一连串动作一步一步连续执行下来:
此时声明:第一次点击红色的recorder按钮,然后手动操作会自动生成测试脚本,第二次commond + U
是进行测试 UI
③UnitTest工具拓展
XCTest一共提供了三种UI测试对象
- XCUIApplication 当前测试应用target
- XCUIElementQuery 定位查询当前UI中xctuielement的一个类
- XCUIElement UI 测试中任何一个item项都被抽象成一个XCUIElement类型
当我们获取了录制生成的代码以后,根据UITest提供的三种对象,我可以在此来对测试代码进行修改,调试UITest中同样适用一下断言等:
XCTAssert(expression, format...)
// Bool测试:
XCTAssertTure(expression, format...)
XCTAssertFalse(expression, format...)
// 相等测试
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...)
// double float 对比数据测试适用
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
// Nil测试,XCTAssert[Not]Nil断言判断给定的表达式值是否为nil
XCTAssertNil(expression, format...)
XCTAssertNotNil(expression, format...)
// 失败断言
XCTFail(format...)
......
关于Xcode UnitTest的问题就讲到此处,希望有兴趣的同学大家共同交流…
在开源社区里,大家对新事物接受得很快,一些好的东西也经常病毒性的扩散。iOS开源社区也是这样,比如,它的测试编写趋势使用xSpec风格,这是从Ruby测试库Rspec借鉴来的。
Kiwi是对XCTest的一个完善替代,使用xSpec风格编写测试。Kiwi带有自己的一套工具集,包括expectation、mocks、stubs,设置还支持单元测试。
Specta与Kiwi功能相似,但在架构上非常不同。Kiwi注重功能的整合,而Specta则注重模块化。它本身只专注于运行测试,而将模拟、匹配等功能交给第三方。下面这些一些开源测试组件,他们能与Specta与Kiwi框架搭配使用:
- Expecta: 匹配程序框架
- OCHamcrest: 匹配程序框架
- OCMock: 模拟测试框架
- OCMokito: 模拟测试框架
- OHTTPStubs: 模拟网络请求的库,基于block的语法来匹配URL
- Nocilla: 模拟网络请求的库,使用链式API
Quick是一个使用Swift开发的新测试框架,对测试使用Swift编写的App非常友好。它还有一个Nimble库用于编写匹配模式。
Quick 例子(example)和组例子(example groups)提供两种目的:
- 他们鼓励你去写测试描述
- 在你的测试“安排”步骤时他们极大地简化代码测试
例子,定义一个它的函数,去了解断言如何在测试用例中使用的。
在XCTest中这些测试方法是相似的。
它接受两个参数:这个例子的名字和一个闭包函数。在下面的例子中详细说明Sea.Dolphin
类的行为,一个海豚应该是聪明和友好的:
// Swift
import Quick
import Nimble
import Sea
class DolphinSpec: QuickSpec {
override func spec() {
it("is friendly") {
expect(Dolphin().isFriendly).to(beTruthy)
}
it("is smart") {
expect(Dolphin().isSmart).to(beTruthy())
}
}
}
// Objective-C
@import Quick;
@import Nimble
QuickSpecBegin(DolphinSpec)
it(@"is friendly", ^ {
expect(@([[Dolphin new] isFriendly])).to(beTruthy());
})
it(@"is smart", ^{
expect(@([[Dolphin new] isSmart])).to(beTruthy());
});
QuickSpecEnd
分组测试的例子是逻辑组的例子,例子能共享代码的setup
和teardown
。
describe
描述类和类的方法这个Dolphin
类的click
方法指定了一种特殊的行为——换句话说:去测试方法的工作-使用describe
可以将它的几个方法放在一个组里进行测试,讲相似的测试分组放在一起使得测试代码更容易阅读:
// Swift
import Quick
import Nimble
Class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
describe("its click") {
it("is loud") {
let click = Dolphin().click()
expect(click.isLoud).to(beTruthy())
}
it() {
let click = Dolphin().click()
expect(click.hasHighFrequency().to(beTruthy())
}
}
}
}
}
// Objective-C
@import Quick
@import Nimble
QuickSpecBegin(DolphinSpec)
describe(@"a dolphin", ^{
describe(@"its click", ^{
it(@"is loud", ^{
Click *click = [[Dolphin new] click];
expect(@(click.isLoud)).to(beTruthy());
});
it(@"has a high frequency", ^{
Click *click = [[Dolphin new] click];
expect(@(click.hasHighFrequency)).to(beTruthy());
});
});
});
QuickSpecEnd
当这两个部分运行在Xcode,它们将会有以下的描述信息在这个describe和他的函数中:
1、DolphinSpec.a_dolphin_its_click_is_loud
2、DolphinSpec.a_dolphin_its_click_has_a_high_frequency
当再一次进行测试的时候;我们可以明确的知道这个测试用例是用来测试什么的;
分组的例子不仅只是使得代码更清晰,在一组测试汇总也可以使用setup
和teardowm
;
在下面的例子中,使用beforeEach
函数去创建dolphin的一个新的实例和在每个测它的clivk
方法之前;这样可以确保每一次的测试类都保持最新的状态:
// Swift
import Quick
import Nimble
class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
var dolphin: Dolphin!
beforeEach {
dolphin = Dolphin()
}
describe("its click") {
var click: Click!
beforeEach {
click = dolphin.click()
}
it("is loud") {
expect(click.isLoud).to(beTruthy())
}
it("has a high frequency") {
expect(click.hasHighFrequency).to(beTruthy())
}
}
}
}
}
// Objective-C
@import Quick;
@import Nimble;
QuickSpecBegin(DolphinSpec)
describe(@"a dolphin", ^{
__block Dolphin *dolphin = nil;
beforeEach(^{
dolphin = [Dolphin new];
});
describe(@"its click", ^{
__block Click *click = nil;
beforeEach(^{
click = [dolphin click];
});
it(@"is loud", ^{
expect(@(click.isLoud)).to(beTruthy());
});
it(@"has a high frequency", ^{
expect(@(click.hasHighFrequency)).to(beTruthy());
});
});
});
QuickSpecEnd
在dolphin
例子中setup
看起来不是那么的必要;但是对于一些更复杂的对象来说,它省去很多的代码量!每次执行这些代码后,都会执行afterEach
。
当海豚发现一些他们感兴趣的事情的事情,通过回声的次数来进行定位,通过释放一系列的声音去确定他是什么。
这个测试展示在不同的环境不同的习惯下click
情景;通常下,海豚只会发出一次震动,但是当他们发现特别有趣的事情的时候,就会发出多次的震动;
这些情况可以使用context
函数:一个正常的context
和当海豚接近感兴趣的东西的context
:
// Swift
import Quick
import Nimble
class DolphinSpec: QuickSpec {
override func spec() {
describe("a dolphin") {
var dolphin: Dolphin!
beforeEach { dolphin = Dolphin() }
describe("its click") {
context("when the dolphin is not near anything interesting") {
it("is only emitted once") {
expect(dolphin!.click().count).to(equal(1))
}
}
context("when the dolphin is near something interesting") {
beforeEach {
let ship = SunkenShip()
Jamaica.dolphinCove.add(ship)
Jamaica.dolphinCove.add(dolphin)
}
it("is emitted three times") {
expect(dolphin.click().count).to(equal(3))
}
}
}
}
}
}
// Objective-C
@import Quick;
@import Nimble;
QuickSpecBegin(DolphinSpec)
describe(@"a dolphin", ^{
__block Dolphin *dolphin = nil;
beforeEach(^{ dolphin = [Dolphin new]; });
describe(@"its click", ^{
context(@"when the dolphin is not near anything interesting", ^{
it(@"is only emitted once", ^{
expect(@([[dolphin click] count])).to(equal(@1));
});
});
context(@"when the dolphin is near something interesting", ^{
beforeEach(^{
[[Jamaica dolphinCove] add:[SunkenShip new]];
[[Jamaica dolphinCove] add:dolphin];
});
it(@"is emitted three times", ^{
expect(@([[dolphin click] count])).to(equal(@3));
});
});
});
});
QuickSpecEnd
严格的来说,context
和describe
是同义词;但是严格的规范化会使你的代码更容易理解;
在Effective Tests Using XCTest: Arrange, Act, and Assert
中,我们看到每一个测试都使用一种方式测试使得测试高效明确。在XCTest,使用一个很长的方法名来表示:
func testDolphin_click_whenTheDolphinIsNearSomethingInteresting_isEmittedThreeTimes() {
// ...
}
使用Quick,这种情况是更加容易去阅读和我们能为每一个测试组去设置:
describe("a dolphin") {
describe("its click") {
context("when the dolphin is near something interesting") {
it("is emitted three times") {
// ...
}
}
}
}
你能够暂时禁用Example
或Groups
是测试时不通过;这个测试实例的名称和结果会打印出来,但是并不会执行该方法;
你可以在方法之前使用x
来禁用测试方法:
// Swift
xdescribe("its click") {
// ...none of the code in this closure will be run.
}
xcontext("when the dolphin is not near anything interesting") {
// ...none of the code in this closure will be run.
}
xit("is only emitted once") {
// ...none of the code in this closure will be run.
}
// Objective-C
xdescribe(@"its click", ^{
// ...none of the code in this closure will be run.
});
xcontext(@"when the dolphin is not near anything interesting", ^{
// ...none of the code in this closure will be run.
});
xit(@"is only emitted once", ^{
// ...none of the code in this closure will be run.
});
有时,它可以使得我们更加专注一个或几个例子;运行一个或两个例子比全部运行更快。使用fit
函数你可以只允许一个或者两个。你也可以使用fbdescribe
或 fcontext
关注一组例子:
//-------swifit
fit("is loud") {
// ...only this focused example will be run.
}
it("has a high frequency") {
// ...this example is not focused, and will not be run.
}
fcontext("when the dolphin is near something interesting") {
// ...examples in this group are also focused, so they'll be run.
}
//-----oc
fit(@"is loud", {
// ...only this focused example will be run.
});
it(@"has a high frequency", ^{
// ...this example is not focused, and will not be run.
});
fcontext(@"when the dolphin is near something interesting", ^{
// ...examples in this group are also focused, so they'll be run.
});
如果你喜欢你也可是使用许多的 beforeSuit
和 afterSuite
;在测试运行之前执行所有的beforeSuite
;在测试完成之后执行所有的afterSuite
,在这里闭包是不能保证所有的按照顺序执行;
KIF (Keep It Functional) 是用Objective-C编写的用户界面测试框架。KIF tester使用私有API来了解App中的视图层级。缺点是运行较慢。
Subliminal是另一款与XCTest集成的框架。与KIF不同的是,它基于UIAutomation编写,旨在对开发者隐藏UIAutomation中一些负责的细节。不过它最后日期是2014年2月,可能已停止更新。
Calabash是跨平台开发工具Xamarin推出的一款测试工具。它使用BDD风格来编写验收测试。它的优点支持跨平台(需使用的库),声明式的测试风格非常易于读写。缺点是不够稳定并且运行速度慢。
这里列出的持续集成工具都是基于云平台的,因为自己搭建类似Jenkins这样的持续集成环境代价高昂。
我会在以后的文章中给出Jenkins搭建方法
下面的列表是目前支持iOS的主流CI平台。
- Travis CI
- Ship.io
- Sauce Labs
- Bitrise
- Testdroid
它们间的不同在于价格、配置工作,以及上手难易程度。
自动化测试就是写一些测试代码,用代码代替人工去完成模块和业务的测试。
其实不管开发还是测试,如果你在不断做重复性工作的时候,就该问自己一个问题:是不是有更高效的方法?
通常,我们会选择那些业务稳定,需要频繁测试的部分来编写自动化测试脚本,其余的采用人工测试,人工测试仍然是iOS App开发中不可缺少的一部分。
从是否接触源代码的角度来分类:测试分为黑盒和白盒(或者就是黑盒白盒结合)。
白盒测试的时候,测试人员是可以直接接触待测试的App的源代码的。白盒测试更多的是单元测试,测试人员针对各个单元进行各种可能的输入分析,然后测试其输出。白盒测试的测试代码通常有iOS开发编写。
黑盒测试,就是测试人员不需要接触源代码进行测试。是从App层面对其行为以及UI的正确性进行验证,黑盒测试由iOS测试完成。
说了这多,自动化测试的效率怎么样,关键还是在测试框架上。那么,如何选择测试框架呢?框架可以分为两大类:Xcode内置的和第三方库:
选择框架的时候有几个方面考虑
准确的测试用例
通常,你的测试用例分为三部分:
测试代码结构
当测试用例多了,你会发现测试代码编写和维护也是一个技术活。通常我们会从几个角度考虑:
组织结构图:
appium采用了Client Server的模式。对于App来说就是一个Server,基于WebDriver JSON wire protocol对实际的UI操作库进行了封装,并且暴露出RESTFUL的接。然后测试代码通过HTTP请求的方式,来进行实际的测试。其中,实际驱动UI的框架根据系统版本有所不同:
原因也必将简单:Apple在10.0之后,移除了UIAutomation的支持,只支持XCUITest。
针对第三方框架,单元测试还是选择BDD框架,毕竟可读性比较高,推荐Quick(Swift),Kiwi(Objective-C)
UI测试优先推荐KIF,如果选哟啊兼顾安卓,可以采用appium
最后奉上参考资料: