网络测试
我们在测试某个方法的时候可能会遇到方法内部调用了网络通信能力:
· 网络请求成功,可能刷新 UI 或者给出一些成功的提示
· 网络失败或者网络不可用则给出一些失败的提示
所以需要对网络通信去看进行模拟。
iOS 中很多网络都是基于 NSURL 系统下的类实现的。所以我们可以利用 NSURLProtocol 的能力来监控网络并 mock 网络数据。
开源项目 OHHTTPStubs 就是一个对网络模拟的库。它可以拦截 HTTP 请求,返回 json 数据,定制各种头信息。
几个主要类及其功能:HTTPStubsProtocol 拦截网络请求、HTTPStubs 单例管理 HTTPStubsDescriptor 实例对象、HTTPStubsResponse 伪造 HTTP 请求。
HTTPStubsProtocol 继承自 NSURLProtocol,可以在 HTTP 请求发送之前对 request 进行过滤处理:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { BOOL found = ([HTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil); if (!found && HTTPStubs.sharedInstance.onStubMissingBlock) { HTTPStubs.sharedInstance.onStubMissingBlock(request); } return found; }
firstStubPassingTestForRequest 方法内部会判断请求是否需要被当前对象处理。
紧接着开始发送网络请求。实际上在 - (void)startLoading 方法中可以用任何网络能力去完成请求,比如NSURLSession、NSURLConnection、AFNetworking 或其他网络框架,OHHTTPStubs 的做法是获取 request、client 对象。
如果 HTTPStubs 单例中包含 onStubActivationBlock 对象,则执行该 block,然后利用 responseBlock 对象返回一个 HTTPStubsResponse 响应对象。
OHHTTPStubs 的具体 API 可以查看文档,文档地址:https://github.com/AliSoftware/OHHTTPStubs/wiki/Usage-Examples。
举个例子,利用 Kiwi、OHHTTPStubs 测试离线包功能,代码如下:
@interface HORouterManager (Unittest) - (void)fetchOfflineInfoIfNeeded; @end SPEC_BEGIN(HORouterTests) describe(@"routerTests", ^{ context(@"criticalPath", ^{ __block HORouterManager *routerManager = nil; beforeAll(^{ routerManager = [[HORouterManager alloc] init]; }); it(@"getLocalPath", ^{ __block NSString *pagePath = nil; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ pagePath = [routerManager filePathOfUrl:@"http://***/resource1"]; }); [[expectFutureValue(pagePath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil]; __block NSString *rescPath = nil; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ rescPath = [routerManager filePathOfUrl:@"http://***/resource1"]; }); [[expectFutureValue(rescPath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil]; }); it(@"fetchOffline", ^{ [HOOfflineManager sharedInstance].offlineInfoInterval = 0; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [request.URL.absoluteString containsString:@"h5-offline-pkg"]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[@"code"] = @(0); dict[@"data"] = @"f722fc3efce547897819e9449d2ac562cee9075adda79ed74829f9d948e2f6d542a92e969e39dfbbd70aa2a7240d6fa3e51156c067e8685402727b6c13328092ecc0cbc773d95f9e0603b551e9447211b0e3e72648603e3d18e529b128470fa86aeb45d16af967d1a21b3e04361cfc767b7811aec6f19c274d388ddae4c8c68e857c14122a44c92a455051ae001fa7f2b177704bdebf8a2e3277faf0053460e0ecf178549e034a086470fa3bf287abbdd0f79867741293860b8a29590d2c2bb72b749402fb53dfcac95a7744ad21fe7b9e188881d1c24047d58c9fa46b3ebf4bc42a1defc50748758b5624c6c439c182fe21d4190920197628210160cf279187444bd1cb8707362cc4c3ab7486051af088d7851846bea21b64d4a5c73bd69aafc4bb34eb0862d1525c4f9a62ce64308289e2ecbc19ea105aa2bf99af6dd5a3ff653bbe7893adbec37b44a088b0b74b80532c720c79b7bb59fda3daf85b34ef35"; NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; return [OHHTTPStubsResponse responseWithData:data statusCode:200 headers:@{@"Content-Type":@"application/json"}]; }]; [routerManager fetchOfflineInfoIfNeeded]; [[HOOfflineInfo shouldEventually] receive:@selector(saveToLocal:)]; }); }); }); SPEC_END
插一嘴,我贴的代码已经好几次可以看到不同的测试框架组合了,所以不是说选了框架 A 就完事,根据场景选择最优解。
UI 测试
上面文章大篇幅的讲了单元测试相关的话题,单元测试十分适合代码质量、逻辑、网络等内容的测试,但是针对最终产物 App 来说单元测试就不太适合了,如果测试 UI 界面的正确性、功能是否正确显然就不太适合了。
Apple 在 Xcode 7 开始推出的 UI Testing 就是苹果自己的 UI 测试框架。
很多 UI 自动化测试框架的底层实现都依赖于 Accessibility,也就是 App 可用性,UI Accessibility 是 iOS 3.0 引入的一个人性化功能,帮助身体不便的人士方便使用 App。
Accessibility 通过对 UI 元素进行分类和标记。分类成类似按钮、文本框、文本等类型,使用 identifier 来区分不同 UI 元素,无痕埋点的设计与实现里面也使用 accessibilityIdentifier 来绑定业务数据。
1、使用 Xcode 自带的 UI测试则在创建工程的时候需要勾选 “Include UI Tests”。
2、像单元测试意义,UI 测试方法命名以 test 开头。将鼠标光标移到方法内,点击 Xcode 左下方的红色按钮,开始录制 UI 脚本。
解释说明:
/*! Proxy for an application that may or may not be running. */ @interface XCUIApplication : XCUIElement // ... @end
XCUIApplication launch 来启动测试,XCUIApplication 是 UIApplication 在测试进程中的代理,用来和 App 进行一些交互。
使用 staticTexts来获取当前屏幕上的静态文本(UILabel)元素的代理。等价于 [app descendantsMatchingType:XCUIElementTypeStaticText]。XCUIElementTypeStaticText 参数是枚举类型。
typedef NS_ENUM(NSUInteger, XCUIElementType) { XCUIElementTypeAny = 0, XCUIElementTypeOther = 1, XCUIElementTypeApplication = 2, XCUIElementTypeGroup = 3, XCUIElementTypeWindow = 4, XCUIElementTypeSheet = 5, XCUIElementTypeDrawer = 6, XCUIElementTypeAlert = 7, XCUIElementTypeDialog = 8, XCUIElementTypeButton = 9, XCUIElementTypeRadioButton = 10, XCUIElementTypeRadioGroup = 11, XCUIElementTypeCheckBox = 12, XCUIElementTypeDisclosureTriangle = 13, XCUIElementTypePopUpButton = 14, XCUIElementTypeComboBox = 15, XCUIElementTypeMenuButton = 16, XCUIElementTypeToolbarButton = 17, XCUIElementTypePopover = 18, XCUIElementTypeKeyboard = 19, XCUIElementTypeKey = 20, XCUIElementTypeNavigationBar = 21, XCUIElementTypeTabBar = 22, XCUIElementTypeTabGroup = 23, XCUIElementTypeToolbar = 24, XCUIElementTypeStatusBar = 25, XCUIElementTypeTable = 26, XCUIElementTypeTableRow = 27, XCUIElementTypeTableColumn = 28, XCUIElementTypeOutline = 29, XCUIElementTypeOutlineRow = 30, XCUIElementTypeBrowser = 31, XCUIElementTypeCollectionView = 32, XCUIElementTypeSlider = 33, XCUIElementTypePageIndicator = 34, XCUIElementTypeProgressIndicator = 35, XCUIElementTypeActivityIndicator = 36, XCUIElementTypeSegmentedControl = 37, XCUIElementTypePicker = 38, XCUIElementTypePickerWheel = 39, XCUIElementTypeSwitch = 40, XCUIElementTypeToggle = 41, XCUIElementTypeLink = 42, XCUIElementTypeImage = 43, XCUIElementTypeIcon = 44, XCUIElementTypeSearchField = 45, XCUIElementTypeScrollView = 46, XCUIElementTypeScrollBar = 47, XCUIElementTypeStaticText = 48, XCUIElementTypeTextField = 49, XCUIElementTypeSecureTextField = 50, XCUIElementTypeDatePicker = 51, XCUIElementTypeTextView = 52, XCUIElementTypeMenu = 53, XCUIElementTypeMenuItem = 54, XCUIElementTypeMenuBar = 55, XCUIElementTypeMenuBarItem = 56, XCUIElementTypeMap = 57, XCUIElementTypeWebView = 58, XCUIElementTypeIncrementArrow = 59, XCUIElementTypeDecrementArrow = 60, XCUIElementTypeTimeline = 61, XCUIElementTypeRatingIndicator = 62, XCUIElementTypeValueIndicator = 63, XCUIElementTypeSplitGroup = 64, XCUIElementTypeSplitter = 65, XCUIElementTypeRelevanceIndicator = 66, XCUIElementTypeColorWell = 67, XCUIElementTypeHelpTag = 68, XCUIElementTypeMatte = 69, XCUIElementTypeDockItem = 70, XCUIElementTypeRuler = 71, XCUIElementTypeRulerMarker = 72, XCUIElementTypeGrid = 73, XCUIElementTypeLevelIndicator = 74, XCUIElementTypeCell = 75, XCUIElementTypeLayoutArea = 76, XCUIElementTypeLayoutItem = 77, XCUIElementTypeHandle = 78, XCUIElementTypeStepper = 79, XCUIElementTypeTab = 80, XCUIElementTypeTouchBar = 81, XCUIElementTypeStatusItem = 82, };
通过 XCUIApplication 实例化对象调用 descendantsMatchingType: 方法得到的是 XCUIElementQuery 类型。比如 @property (readonly, copy*) XCUIElementQuery *staticTexts;:
/*! Returns a query for all descendants of the element matching the specified type. */ - (XCUIElementQuery *)descendantsMatchingType:(XCUIElementType)type;
descendantsMatchingType 返回所有后代的类型匹配对象。childrenMatchingType 返回当前层级子元素的类型匹配对象:
/*! Returns a query for direct children of the element matching the specified type. */ - (XCUIElementQuery *)childrenMatchingType:(XCUIElementType)type;
拿到 XCUIElementQuery 后不能直接拿到 XCUIElement。和 XCUIApplication 类似,XCUIElement 不能直接访问 UI 元素,它是 UI 元素在测试框架中的代理。可以通过 Accessibility 中的 frame、identifier 来获取。
对比很多自动化测试框架都需要找出 UI 元素,也就是借助于 Accessibility 的 identifier。
第三方 UI 自动化测试框架挺多的,可以查看下典型的 appium、macaca。
测试经验总结
TDD 写好测试再写业务代码,BDD 先写实现代码,再写基于行为的测试代码。
另一种思路是没必要针对每个类的私有方法或者每个方法进行测试,因为等全部功能做完后针对每个类的接口测试,一般会覆盖据大多数的方法。等测试完看如果方法未被覆盖,则针对性的补充 Unit Test。
目前,UI 测试(appium) 还是建议在核心逻辑且长时间没有改动的情况下去做,这样子每次发版本的时候可以当作核心逻辑回归了,目前来看价值是方便后续的迭代和维护上有一些便利性,其他的功能性测试还是走 BDD。
对于类、函数、方法的走 TDD,老老实实写 UT、走 UT 覆盖率的把控。
UITesting 还是建议在核心逻辑且长时间没有改动的情况下去做,这样子每次发版本的时候可以当作核心逻辑回归,目前来看价值是方便后续的迭代和维护上有一些便利性。例如用户中心 SDK 升级后,当时有了UITesing,基本上免去了测试人员介入。
如果是一些活动页和逻辑经常变动的,老老实实走测试黑盒……
我觉得一直有个误区,就是觉得自动测试是为了质量,其实质量都是附送的,测试先行是让开发更快更爽的。
WWDC 这张图也很清楚,UI 其实需要的占比较小,还是要靠单测驱动。
作者:杭城小刘