iOS自动化测试的那些干货

前言html

若是有测试大佬发现内容不对,欢迎指正,我会及时修改。android

大多数的iOS App(没有持续集成)迭代流程是这样的ios

20170221102031324.png

也就是说,测试是发布以前的最后一道关卡。若是bug不能在测试中发现,那么bug就会抵达用户,因此测试的完整性可靠性十分重要。git

目前,大多数App还停留在人工测试阶段,人工测试投入的成本最低,可以保证核心功能的使用,并且测试人员不须要会写代码。github

可是,在不少测试场景下,人工测试的效率过低,容易出错。举两个常见的例子:web

  • 一个App的核心功能,在每一次发布版本前的测试一定会跑一遍全部的测试用例,无论对应的业务在当前版本有没有变化(天知道开发在作业务A的时候,对业务B有没有影响),若是此次测出新的bug,测试人员在下一次发版测试中,又不得不作这些重复的工做。编程

  • 开发在写API请求相关代码的时候没有作数据容错,测试在人工测试的时候都是正常的数据,因此测试经过。上线了以后,后台配置数据的时候出了点小问题,致使大面积崩溃,boom~。swift

而后,老板就要过来找你了xcode

 

20170309120143195.jpg

本文所讲解的均是基于XCode 8.2.1,有些概念可能不适用于低版本的XCode缓存

自动化测试

自动化测试就是写一些测试代码,用代码代替人工去完成模块和业务的测试。

其实无论是开发仍是测试,若是你在不断的作重复性工做的时候,就应该问本身一个问题:是否是有更高效的办法?

自动化测试有不少优势:

  • 测试速度快,避免重复性的工做

  • 避免regression,让开发更有信心去修改和重构代码(我的认为最大的优势)

  • 具备一致性。

  • 有了自动化测试,持续集成(CI)会变得更可靠。

  • 迫使开发人员写出更高质量的代码。(自动化测试不经过,代码不容许合并)

固然,自动化测试也有一些缺点。

  • 开发和维护成本高。

  • 不能彻底替代人工测试。

  • 没法彻底保证测试的准确性 - 让代码去判断一段逻辑是否正确很容易,可是,让代码判断一个控件显示是否正确却没那么容易。

因此,在作自动化测试以前,首先要问本身几个问题?

  • 这个测试业务的变更是否频繁?

  • 这个测试业务是否属于核心功能?

  • 编写测试代码的成本有多少?

  • 自动化测试能保证测试结果的准确么?

一般,咱们会选择那些业务稳定,须要频繁测试的部分来编写自动化测试脚本,其他的采用人工测试,人工测试仍然是iOS App开发中不可缺乏的一部分。

测试种类

从是否接触源代码的角度来分类:测试分为黑盒和白盒(灰盒就是黑盒白盒结合,这里不作讨论)。

白盒测试的时候,测试人员是能够直接接触待测试App的源代码的。白盒测试更多的是单元测试,测试人员针对各个单元进行各类可能的输入分析,而后测试其输出。白盒测试的测试代码一般由iOS开发编写。

黑盒测试。黑盒测试的时候,测试人员不须要接触源代码。是从App层面对其行为以及UI的正确性进行验证,黑盒测试由iOS测试完成。

从业务的层次上来讲,测试金字塔如图:

20170305180528100.png

而iOS测试一般只有如下两个层次:

  • Unit,单元测试,保证每个类可以正常工做

  • UI,UI测试,也叫作集成测试,从业务层的角度保证各个业务能够正常工做。

框架选择

啰里八嗦讲的这么多,自动化测试的效率怎么样,关键仍是在测试框架上。那么,如何选择测试框架呢?框架能够分为两大类:XCode内置的和三方库。

选择框架的时候有几个方面要考虑

  • 测试代码编写的成本

  • 是否可调式

  • 框架的稳定性

  • 测试报告(截图,代码覆盖率,…)

  • WebView的支持(不少App都用到了H5)

  • 自定义控件的测试

  • 是否须要源代码

  • 可否须要连着电脑

  • 是否支持CI(持续集成)

  • ….

咱们首先来看看XCode内置的框架:XCTest。XCTest又能够分为两部分:Unit Test 和 UI Test,分别对应单元测试和UI测试。有一些三方的测试库也是基于XCTest框架的,这个在后文会讲到。因为是Apple官方提供的,因此这个框架会不断完善。

成熟的三方框架一般提供了不少封装好的有好的接口,笔者综合对比了一些,推荐如下框架:

单元测试:

如下三个框架都是BDD(Behavior-driven development) - 行为驱动开发。行为驱动开发简单来讲就是先定义行为,而后定义测试用例,接着再编写代码。 实践中发现,一般没有那么多时间来先定义行为,不过BDD中的domain-specific language (DSL)可以很好的描述用例的行为。

  • Kiwi 老牌测试框架

  • specta 另外一个BDD优秀框架

  • Quick 三个项目中Star最多,支持OC和Swift,优先推荐。

UI测试

  • KIF 基于XCTest的测试框架,调用私有API来控制UI,测试用例用Objective C或Swift编写。

  • appium 基于Client - Server的测试框架。App至关于一个Server,测试代码至关于Client,经过发送JSON来操做APP,测试语言能够是任意的,支持android和iOS。

篇幅有限,本文会先介绍XCtest,接着三方的Unit框架会以Quick为例,UI Test框架侧重分析KIF,appium仅仅作原理讲解。

XCTest

对于XCTest来讲,最后生成的是一个bundle。bundle是不能直接执行的,必须依赖于一个宿主进程。关于XCTest进行单元测试的基础(XCode的使用,异步测试,性能测试,代码覆盖率等),我在这篇文章里讲解过,这里再也不详细讲解。

单元测试用例

好比,我有如下一个函数:

1
2
3
//验证一段Text是否有效。(不能以空字符开头,不能为空)
- (BOOL)validText:(NSString *)text error:(NSError *__autoreleasing *)error{
}

那么,我该如何为这个函数编写单元测试的代码?一般,须要考虑如下用例:

  • 输入以空白字符或者换行符开头的,error不为空,返回 NO

  • 输入正确的内容,error为空,返回YES

  • 输入为nil,error不为空,返回 NO (边界条件)

  • 输入为非NSString类型,验证不经过,返回NO (错误输入)

  • 特殊输入字符(标点符号,非英文等等)

UI测试

UI测试是模拟用户操做,进而从业务处层面测试。关于XCTest的UI测试,建议看看WWDC 2015的这个视频:

关于UI测试,有几个核心类须要掌握

UI测试还有一个核心功能是UI Recording。选中一个UI测试用例,而后点击图中的小红点既能够开始UI Recoding。你会发现:

随着点击模拟器,自动合成了测试代码。(一般自动合成代码后,还须要手动的去调整)

20170221171911419.png

在写UI测试用例的时候要注意:测试行为而不是测试代码。好比,咱们测试这样一个case

进入Todo首页,点击add,进入添加页面,输入文字,点击save。

测试效果以下:

20170309115140207.gif

对应测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)testAddNewItems{
     //获取app代理
     XCUIApplication *app = [[XCUIApplication alloc] init];
     //找到第一个tabeview,就是咱们想要的tableview
     XCUIElement * table = [app.tables elementBoundByIndex:0];
     //记录下来添加以前的数量
     NSInteger oldCount = table.cells.count;
     //点击Add
     [app.navigationBars[@ "ToDo" ].buttons[@ "Add" ] tap];
     //找到Textfield
     XCUIElement *inputWhatYouWantTodoTextField = app.textFields[@ "Input what you want todo" ];
     //点击Textfield
     [inputWhatYouWantTodoTextField tap];
     //输入字符
     [inputWhatYouWantTodoTextField typeText:@ "somethingtodo" ];
     //点击保存
     [app.navigationBars[@ "Add" ].buttons[@ "Save" ] tap];
     //获取当前的数量
     NSInteger newCount = table.cells.count;
     //若是cells的数量加一,则认为测试成功
     XCTAssert(newCount == oldCount + 1);
}

这里是经过先后tableview的row数量来断言成功或者失败。

等待

一般,在视图切换的时候有转场动画,咱们须要等待动画结束,而后才能继续,不然query的时候极可能找不到咱们想要的控件。

好比,以下代码等待VC转场结束,当query只有一个table的时候,才继续执行后续的代码。

1
2
3
4
5
[self expectationForPredicate:[NSPredicate predicateWithFormat:@ "self.count = 1" ]
           evaluatedWithObject:app.tables
                       handler:nil];
[self waitForExpectationsWithTimeout:2.0 handler:nil];
//后续代码....

Tips: 当你的UI结构比较复杂的时候,好比各类嵌套childViewController,使用XCUIElementQuery的代码会很长,也很差维护。

另外,UI测试还会在每一步操做的时候截图,方便对测试报告进行验证。

查看测试结果

使用基于XCTest的框架,能够在XCode的report navigator中查看测试结果。

 

其中:

  • Tests 用来查看详细的测试过程

  • Coverage 用来查看代码覆盖率

  • Logs 用来查看测试的日志

点击图中的红色框指向的图标能够看到每一步UI操做的截图

除了利用XCode的GUI,还能够经过后文提到的命令行工具来测试,查看结果。

Stub/Mock

首先解释两个术语:

  • mock 表示一个模拟对象

  • stub 追踪方法的调用,在方法调用的时候返回指定的值。

一般,若是你采用纯存的XCTest,推荐采用OCMock来实现mock和stub,单元测试的三方库一般已集成了stub和mock。

那么,如何使用mock呢?举个官方的例子:

1
2
3
4
5
//mock一个NSUserDefaults对象
id userDefaultsMock = OCMClassMock([NSUserDefaults class]);
//在调用stringForKey的时候,返回http://testurl
OCMStub([userDefaultsMock 
stringForKey:@ "MyAppURLKey" ]).andReturn(@ "http://testurl" );

再好比,咱们要测试打开其余App,那么如何判断确实打开了其余App呢?

1
2
3
id app = OCMClassMock([UIApplication class]);
OCMStub([app sharedInstance]).andReturn(app);
OCMVerify([app openURL:url]

使用Stub可让咱们很方便的实现这个。

关于OCMock的使用,推荐看看objc.io的这篇文章

Quick

Quick是创建在XCTestSuite上的框架,使用XCTestSuite容许你动态建立测试用例。因此,使用Quick,你仍让可使用XCode的测试相关GUI和命令行工具。

使用Quick编写的测试用例看起来是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Quick
import Nimble
 
class TableOfContentsSpec: QuickSpec {
   override func spec() {
     describe( "the 'Documentation' directory" ) {
       it( "has everything you need to get started" ) {
         let sections = Directory( "Documentation" ).sections
         expect(sections).to(contain( "Organized Tests with Quick Examples and Example Groups" ))
         expect(sections).to(contain( "Installing Quick" ))
       }
       
       context( "if it doesn't have what you're looking for" ) {
         it( "needs to be updated" ) {
           let you = You(awesome:  true )
           expect{you.submittedAnIssue}.toEventually(beTruthy())
         }
       }
     }
   }
}

BDD的框架让测试用例的目的更加明确,测试是否经过更加清晰。使用Quick,测试用例分为两种:

单独的用例 - 使用it来描述

it有两个参数,

  • 行为描述

  • 行为的测试代码

好比,如下测试Dolphin行为,它具备行为is friendly和is smart

1
2
3
4
5
6
7
8
9
10
11
12
//Swift代码
class DolphinSpec: QuickSpec {
   override func spec() {
     it( "is friendly" ) {
       expect(Dolphin().isFriendly).to(beTruthy())
     }
     
     it( "is smart" ) {
       expect(Dolphin().isSmart).to(beTruthy())
     }
   }
}

能够看到,BDD的核心是行为。也就是说,须要关注的是一个类提供哪些行为。

用例集合,用describe和context描述

好比,验证dolphin的click行为的时候,咱们须要两个用例。一个是is loud,一个是has a high frequency,就能够用describe将用例组织起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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( "has a high frequency" ) {
           let click = Dolphin().click()
           expect(click.hasHighFrequency).to(beTruthy())
         }
       }
     }
   }
}

context能够指定用例的条件:

好比

1
2
3
4
5
6
7
describe( "its click" ) {
     context( "when the dolphin is not near anything interesting" ) {
       it( "is only emitted once" ) {
         expect(dolphin!.click().count).to(equal(1))
       }
     }
}

除了这些以外,Quick也支持一些切入点,进行测试前的配置:

  • beforeEach

  • afterEach

  • beforeAll

  • afterAll

  • beforeSuite

  • afterSuite

Nimble

因为Quick是基于XCTest,开发者固然能够收使用断言来定义测试用例成功或者失败。Quick提供了一个更有好的Framework来进行这种断言:Nimble

好比,一个常见的XCTest断言以下:

1
XCTAssertTrue(ConditionCode,  "FailReason" )11

在出错的时候,会提示

1
XCAssertTrue failed, balabala

这时候,开发者要打个断点,查看下上下文,看看具体失败的缘由在哪。

使用Nimble后,断言变成相似

1
2
3
4
expect(1 + 1).to(equal(2))
expect(3) > 2
expect( "seahorse" ).to(contain( "sea" ))
expect([ "Atlantic" "Pacific" ]).toNot(contain( "Mississippi" ))

而且,出错的时候,提示信息会带着上下文的值信息,让开发者更容易的找到错误。

让你的代码更容易单元测试

测试的准确性和工做量很大程度上依赖于开发人员的代码质量。

一般,为了单元测试的准确性,咱们在写函数(方法)的时候会借鉴一些函数式编程的思想。其中最重要的一个思想就是

  • pure function(纯函数)

何为Pure function?就是若是一个函数的输入同样,那么输出必定同样。

好比,这样的一个函数就不是pure function。由于它依赖于外部变量value的值。

1
2
3
4
5
6
static NSInteger value = 0;
 
- (NSInteger)function_1{
     value = value + 1;
     return  value;
}

而这个函数就是pure function,由于给定输入,输出必定一致。

1
2
3
4
- (NSInteger)function_2:(NSInteger)base{
     NSInteger value = base + 1;
     return  value;
}

因此,若是你写了一个没有参数,或者没有返回值的方法,那么你要当心了,极可能这个方法很难测试。

关于MVC

在良好的MVC架构的App中,

  • View只作纯粹的展现型工做,把用户交互经过各类方式传递到外部

  • Model只作数据存储类工做

  • Controller做为View和Model的枢纽,每每要和不少View和Model进行交互,也是自动化包括代码维护的痛点。

因此,对Controller瘦身是iOS架构中比较重要的一环,一些通用的技巧包括:

逻辑抽离:

  • 网络请求独立。能够每一个网络请求以Command模式封装成一个对象,不要直接在Controller调用AFNetworking。

  • 数据存储独立。创建独立的Store类,用来作数据持久化和缓存。

  • 共有数据服务化(协议)。好比登陆状态等等,经过服务去访问,这样服务提供者之须要处理服务的质量,服务使用者则信任服务提供者的结果。

Controller与View解耦合

  • 创建ViewModel层,这样Controller只须要和ViewModel进行交互。

  • 创建UIView子类做为容器,将一些View放到容器后再把容器做为SubView添加到Controller里

  • 创建可复用的Layout层,无论是AutoLayout仍是手动布局。

Controller与Controller解耦合

  • 创建页面路由。每个界面都抽象为一个URL,跳转仅仅经过Intent或者URL跳转,这样两个Controller彻底独立。

若是你的App用Swift开发,那么面向协议编程和不可变的值类型会让你的代码更容易测试。

固然,iOS组建化对自动化测试的帮助也很大,由于无论是基础组件仍是业务组件,均可以独立测试。组建化又是一个很大的课题,这里不深刻讲解了。

KIF

KIF的全称是Keep it functional。它是一个创建在XCTest的UI测试框架,经过accessibility来定位具体的控件,再利用私有的API来操做UI。因为是创建在XCTest上的,因此你能够完美的借助XCode的测试相关工具(包括命令行脚本)。

> KIF是我的很是推荐的一个框架,简单易用。

使用KIF框架强制要求你的代码支持accessibility。若是你以前没接触过,能够看看Apple的文档

简单来讲,accessibility可以让视觉障碍人士使用你的App。每个控件都有一个描述AccessibilityLabel。在开启VoiceOver的时候,点击控件就能够选中而且听到对应的描述。

一般UIKit的控件是支持accessibility的,自定定义控件能够经过代码或者Storyboard上设置。

在Storyboard上设置:

20170226113858569.png

  • 上面的经过Runtime Attributes设置(KVC)

  • 下面的经过GUI来设置

经过代码设置:

1
2
3
[alert setAccessibilityLabel:@ "Label" ];
[alert setAccessibilityValue:@ "Value" ];
[alert setAccessibilityTraits:UIAccessibilityTraitButton];

若是你有些Accessibility的经验,那么你确定知道,像TableView的这种不该该支持VoiceOver的。咱们能够用条件编译来只对测试Target进行设置:

1
2
3
4
5
6
7
#ifdef DEBUG
[tableView setAccessibilityValue:@ "Main List Table" ];
#endif
 
#ifdef KIF_TARGET (这个值须要在build settings里设置)
[tableView setAccessibilityValue:@ "Main List Table" ];
#endif

使用KIF主要有两个核心类:

  • KIFTestCase XCTestCase的子类

  • KIFUITestActor 控制UI,常见的三种是:点击一个View,向一个View输入内容,等待一个View的出现

咱们用KIF来测试添加一个新的ToDo

1
2
3
4
5
6
7
- (void)testAddANewItem{
     [tester tapViewWithAccessibilityLabel:@ "Add" ];
     [tester enterText:@ "Create a test to do item"  intoViewWithAccessibilityLabel:@ "Input what you want todo" ];
     [tester tapViewWithAccessibilityLabel:@ "Save" ];
     [tester waitForTimeInterval:0.2];
     [tester waitForViewWithAccessibilityLabel:@ "Create a test to do item" ];
}

命令行

自动化测试中,命令行工具能够facebook的开源项目:

这是一个基于xcodebuild命令的扩展,在iOS自动化测试和持续集成领域颇有用,并且它支持-parallelize并行测试多个bundle,大大提升测试效率。

安装XCTool,

1
brew install xctool11

使用

1
2
3
4
5
path/to/xctool.sh \
   -workspace YourWorkspace.xcworkspace \
   -scheme YourScheme \
   -reporter plain:/path/to/plain-output.txt \
   run-test

而且,xctool对于持续集成颇有用,iOS经常使用的持续集成的server有两个:

  • Travis CI 对于公开仓库(好比github)免费,私有仓库收费

  • Jenkins 免费

优化你的测试代码

准确的测试用例

一般,你的你的测试用例分为三部分:

  • 配置测试的初始状态

  • 对要测试的目标执行代码

  • 对测试结果进行断言(成功 or 失败)

测试代码结构

当测试用例多了,你会发现测试代码编写和维护也是一个技术活。一般,咱们会从几个角度考虑:

  • 不要测试私有方法(封装是OOP的核心思想之一,不要为了测试破坏封装)

  • 对用例分组(功能,业务类似)

  • 对单个用例保证测试独立(不受以前测试的影响,不影响以后的测试),这也是测试是否准确的核心。

  • 提取公共的代码和操做,减小copy/paste这类工做,测试用例是上层调用,只关心业务逻辑,不关心内部代码实现。

一个常见的测试代码组织以下:

20170227111054666.png

appium

appium采用了Client Server的模式。对于App来讲就是一个Server,基于WebDriver JSON wire protocol对实际的UI操做库进行了封装,而且暴露出RESTFUL的接口。而后测试代码经过HTTP请求的方式,来进行实际的测试。其中,实际驱动UI的框架根据系统版本有所不一样:

  • < 9.3 采用UIAutomation

  • >= 9.3 XCUITest

缘由也比较简单:Apple在10.0以后,移除了UIAutomation的支持,只支持XCUITest。

20170309114024262.png

 

对比KIF,appium有它的优势:

  • 跨平台,支持iOS,Android

  • 测试代码能够由多种语言编写,这对测试来讲门槛更低

  • 测试脚本独立与源代码和测试框架

固然,任何框架都有缺点:

  • 自定义控件支持很差

  • WebView的支持很差

总结

因为我不是专业的iOS测试,关于测试的一点看法以下:

单元测试仍是选择BDD框架,毕竟可读性高一些,推荐Quick(Swift),Kiwi(Objective C)

UI测试优先推荐KIF,若是须要兼顾安卓测试,或者测试人员对OC/Swift很陌生,能够采用appium

参考资料

相关文章
相关标签/搜索