第6章 iOS测试框架二次开发
3.2底层驱动层
这一节主要讲解底层驱动层的实现方式、实现原理、主要功能和代码介绍。
3.2.1XCTest接口封装
(1)XCTset介绍
XCTest 的优势和缺点都是由于它太简单了。你只需要创建一个类,使用 “test” 作为测试方法名的前缀这样就可以了,不需要再做其它的。和 Xcode 很好的集成性也是 XCTest 获得青睐的原因。可以点击边栏上的小菱形按钮来运行测试用例,如图6-3所示。可以很容易的查看所有失败的测试用例,也可以在测试用例列表中点击某一行而快速的跳转到某一个测试用例。
图6-1 XCTEST执行用例图
不幸的是,这已经是 XCTest 的全部优点了。在开发和测试中,使用 XCTest 时我们没有碰到任何的障碍,但是经常会想如果它能更方便一些就好了。XCTest 类看起来就像普通的类,而一个 BDD(行为驱动开发)测试套件的结构和其嵌套的上下文是显而易见的。并且这种为测试创建嵌套上下文的可能性也是最缺失的。嵌套的上下文允许我们在使独立的测试相对简单的情况下创建越来越具体的场景。当然,在 XCTest 中这也是可以的,比如在一些测试用例中调用自定义的setup 方法,但这并不方便。
(2) XCTest工作方式
苹果提供了一些关于如何使用XCTest的官方文档。测试用例被分到继承XCTestCase的不同子类中去。每个以test为开头的方法都是一个测试用例。
因为测试用例都是简单的类和方法,所以我们可以适当地添加一些@property和辅助方法。
注意考虑到代码的重用性,我们的所有测试用例类都有一个共同的父类,也就是TestCase,它也是XCTestCase的子类,所有的测试类都是TestCase类的子类。然后把一些公用的辅助方法放在TestCase类中,并且加了一些属性作为每个测试的预置属性。
(3)XCTest接口类
可以根据Given-When-Then模式来组织我们的测试用例,将测试用例拆分成三个部分。
在given部分里,通过创建模型对象或将被测试的系统设置到指定的状态,来设定测试环境。when这部分包含了我们要测试的代码。在大部分情况,这里只有一个方法调用。在then这部分中 ,需要检查行为的结果:是否得到了期望的结果?对象是否有改变?这部分主要包括一些断言。主要的接口使用如下代码所示。
#import <XCTest/XCTestDefines.h> #import<XCTest/XCTestErrors.h> #import<XCTest/XCAbstractTest.h> #import<XCTest/XCTestAssertions.h> #import<XCTest/XCTestAssertionsImpl.h> #import<XCTest/XCTestCase.h> #import<XCTest/XCTestCase+AsynchronousTesting.h> #import<XCTest/XCTestCaseRun.h> #import<XCTest/XCTestExpectation.h> #import<XCTest/XCTestLog.h> #import<XCTest/XCTestObserver.h> #import<XCTest/XCTestObservationCenter.h> #import<XCTest/XCTestObservation.h> #import<XCTest/XCTestProbe.h> #import<XCTest/XCTestRun.h> #import<XCTest/XCTestSuite.h> #import<XCTest/XCTestSuiteRun.h> #import<XCTest/XCUIAPPlication.h> #import<XCTest/XCUIDevice.h> #import<XCTest/XCUICoordinate.h> #import<XCTest/XCUIElement.h> #import<XCTest/XCUIElementQuery.h> #import<XCTest/XCUIElementTypes.h> #import<XCTest/XCUIElementAttributes.h> #import<XCTest/XCUIElementTypeQueryProvider.h> #import<XCTest/XCUIKeyboardKeys.h> #import<XCTest/XCUIRemote.h> |
这种简单的模式使人能够更容易地书写和理解这些测试用例,因为它们都遵循了同样的模式。
(4)可重用代码
随着项目时间推移,注意到在我们的测试用例中有越来越多的重复代码,比如等待异步才能完成,或者设置一个内存中的Core Data 堆栈等操作。为了避免代码重复,整理所有有用的代码片段,并将它们加入到一个公共类中,为所有的测试用例服务。
结果证明这个公共类非常实用。这个测试基础类能够运行自的-setUp和-tearDown方法来配置环境。代码实现如下所示。
-(void)setUp{ self.continueAfterFailure=false; [super setUp]; } -(void)tearDown{ // Put teardown code here. This method is called after the invocation of each test method in the class. [supertearDown]; [selfendScriptLog]; } |
另外一个我们最近开始用的模式也很有用,就是在XCTestCase类中直接实现委托协议。通过这个方式,我们不用必须笨拙地mock这个delegate。相反的,我们可以相当直接地与被测试的类互动。
(5)状态性和无状态性
无状态性的代码在过去几年中一直被提起。但是在现今,我们的 APP 还是需要状态。如果没有状态,大部分 APP 就会变得没有意义。但是状态的管理又很容易引起很多 Bug,因为管理状态非常复杂。通过隔离这些状态来使我们的代码更好的运行。一些类中包含状态,而大部分则是无状态的。通过这样的方式之后,不仅是代码变得更加简单,测试用例也是如此。
比如说,有一个叫EventSync的类,它是负责把本地变化发送到服务器。所以它需要跟踪哪些本地对象发生变化需要上传到服务器,还有哪些本地变化现在正在被上传到服务器。一次需要发送多个变化,但是我们不想发送重复的变化。
我们也有跟踪对象之间的依赖关系。当A和B有依赖关系,并且B有本地变化,那么在发送A的本地变化之前,需要先等待B的本地变化发送完毕。
假如有一个UserSyncStrategy类,它有一个-nextRequest方法可以生成下一次请求。这个请求会将本地改变发送到服务器。虽然这个类本身是无状态的。更确切地说,所有它的状态都被封装在一个叫UpstreamObjcetSync的类中,这个类负责跟踪那些有本地变化的用户对象,还有那些我们正在运行的请求。除了这个类之外其它东西都是没有状态的。
通过这个方式可以很容易得到测试UpstremObjectSync的集合。它们检查这个类是否正确地管理状态。对于UserSyncStrategy来说,在mockUpstremObjectSync的时候,就不用再担心UserSyncStrategy本身的状态了。这大大减少了测试的复杂度,更进一步,因为正在同步很多不同类型的对象,那些不同的类都是无状态的,并且可以重用UpstreamObjectSync类,这使代码简单了很多。
(6)重写XCTest接口
有时候我们需要重新实现一些底层功能,比如我们要重新定义脚本执行方式和获取形式,我们需要重写接口testInvocation,实例代码如下图所示,我们首先在头文件声明该接口,明确要继承XCTestCase类,如下代码所示。
#import <Foundation/Foundation.h> #import <XCTest/XCTest.h> /** Example of plugin that will NSLog all retain cycles found within . This could, for instance, send it somewhere to the backend. */ @interface MttInvocations : XCTestCase @end |
接下来就可以在.m文件实现该重写方式。实现截图部分如下。
+ (NSArray <NSInvocation *> *)testInvocations { // Get the selectors of this class unsigned int mc = 0; Method *mlist = class_copyMethodList(self.class, &mc); NSMutableArray *selectorNames = [NSMutableArray array]; for (unsigned int i = 0; i < mc; i++) { NSString *name = [NSString stringWithFormat:@"%s", sel_getName(method_getName(mlist[i]))]; if (name.length > 4 && [[name substringToIndex:4] isEqualToString:@"test"]) { [selectorNames addObject:name]; } } // Sort them alphabetically [selectorNames sortUsingComparator:^NSComparisonResult(NSString * _Nonnull sel1, NSString * _Nonnull sel2) { return [sel1 compare:sel2]; }]; // Build the NSArray with NSInvocations NSMutableArray *result = [NSMutableArray array]; for (NSString *selectorString in selectorNames) { SEL selector = NSSelectorFromString(selectorString); NSMethodSignature *methodSignature = [self instanceMethodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocation.selector = selector; [result addObject:invocation]; } return result; } |
(7)测试异步性
有的时候我们需要异步测试等待一些交互消息,比如我们需要后后台交互一些异步的http协议信息时候,我们就不能同步的执行操作了,这个时候就需要异步操作,才能保证在等待消息的时候,资源不被释放掉。
需要定义一个XCTestExpectation对象,然后在异步block调用函数里面采用fulfill机制,实现异步操作。设定超时时机等待机制。当超时的时候我们要设置expectation = nil防止该内存提前被ARC释放掉。
经过以上几个方面的封装,可以轻松的操作XCTest接口进行下一步的处理操作,还可以使用XCDOE底层的一些特性。为自动化测试框架的开发立下基础。
3.2.2消息处理模块
该模块主要是处理自动化框架和被测试APP之间的信息交互事件。
模块实现基于的多进程自动化测试框架下实现的一种信息交互机制。我们的实现是基于自动化测试框架开发的,只要是基于多进程控制的UI自动化测试框架,都可以应用该发明实现可控截屏机制。其主要的实现原理如下,该框架的运行机制不是单进程的,而是多进程的运行方式,框架编译运行后,会有测试进程A,被测试APP进程B。所以就需要进程A控制被测试进程B,进行消息控制方式交互。但是iOS系统是不支持多进程控制的。所以我们就需要一种全新的可控的操作方式,以进程A为载体控制被测试进程B,进行消息传递。首先需要在被测试APP中,嵌入服务监听程序,该程序主要是监听约定好的手机端口,建立session连接,注册消息事件,解析事件报文,回调事件函数,获取事件,封装结果报文,返回消息报文。其次在测试进程A中,需要有客户端控制功能,获取到约定的被测试进程APP的监控地址,然后向改地址建立session连接,回调消息事件函数,发送消息事件报文,监控回传的结果报文,解析结果报文,获取消息事件结果,断开连接服务。
其实现代码结构主要如图6-4所示。
图6-2 消息处理代码结构图
主要的功能就是session报文的封装和发送机制,其具体实现代码如下图所示。
import Foundation import UIKit import XCTest @objc public class Session :NSObject { private struct SessionData { static var UITestServerAddress = "http://localhost:14466" static var session: NSURLSession? } public var UITestServerAddress: String { get { return SessionData.UITestServerAddress } set { SessionData.UITestServerAddress = newValue } } var session: NSURLSession { get { if SessionData.session == nil { let sessionConfig = NSURLSessionConfiguration.ephemeralSessionConfiguration() //SessionData.session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: NSOperationQueue.mainQueue()) SessionData.session = NSURLSession(configuration: sessionConfig) } return SessionData.session! } } func urlForEndpoint(endpoint: String, args: [String]) -> NSURL? { var urlString = "\(SessionData.UITestServerAddress)/\(endpoint)" for arg in args { urlString += "/" urlString += arg.urlencode() } let endpoint = NSURL(string: urlString) guard let url = endpoint else { NSLog("Invalid URL: \(urlString)") return nil } return url } func dataFromRemoteEndpoint(endpoint: String, method: String, args: [String]) -> NSData? { guard let url = urlForEndpoint(endpoint, args: args) else { return nil } let request = NSMutableURLRequest(URL: url) request.HTTPMethod = method var result: NSData? = nil let XCTest = XCTestCase() let expectation = XCTest.expectationWithDescription("dataTask") let dataTask = session.dataTaskWithRequest(request) { data, response, error in // WARNING: NOT a main queue if error != nil { NSLog("dataTaskWithRequest error (please check if UITestServer is running): \(error)") return } if let httpResponse = response as? NSHTTPURLResponse { if httpResponse.statusCode != 200 { NSLog("dataTaskWithRequest: status code \(httpResponse.statusCode) received, please check if UITestServer is running") return } } guard let responseData = data else { NSLog("No data received (UITestServer not running?)") return } result = responseData expectation.fulfill() } /*guard let task = dataTask else { NSLog("Unable to create dataTask") return nil }*/ dataTask.resume() XCTest.waitForExpectationsWithTimeout(10.0, handler: nil) return result } func dataFromRemoteEndpoint(endpoint: String, method: String = "GET", args: String...) -> NSData? { return dataFromRemoteEndpoint(endpoint, method: method, args: args) } func stringFromRemoteEndpoint(endpoint: String, method: String, args: [String]) -> String { let data = dataFromRemoteEndpoint(endpoint, method: method, args: args) if let stringData = data { let resolution = NSString(data: stringData, encoding: NSUTF8StringEncoding) ?? "" return resolution as String } return "" } func stringFromRemoteEndpoint(endpoint: String, method: String = "GET", args: String...) -> String { return stringFromRemoteEndpoint(endpoint, method: method, args: args) } func callRemoteEndpoint(endpoint: String, method: String, args: [String]) { let _ = dataFromRemoteEndpoint(endpoint, method: method, args: args) } func callRemoteEndpoint(endpoint: String, method: String = "GET", args: String...) { callRemoteEndpoint(endpoint, method: method, args: args) } } |
本文选自《腾讯iOS测试实践》第六章,本站经机械工业出版社和作者的授权。
版权声明:51Testing软件测试网获机械工业出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。