iOS UI自动化测试

UI testing

找到交互的UI控件,检测UI控件的属性和状态

生成测试报告,包括每步的截图

核心技术包括XCTest 和 Accessibility

XCTest

Xcode's testing framework

Requirements

UI testing depends on new OS features

  • iOS 9
  • OS X 10.11

Getting Started

  • Xcode target type
  • APIs
  • UI recording
UI Testing Xcode Targets

UI tests have special requirements

  • Execute in aseparate process
  • Permission to use Accessibility

New Xcode target templates

  • Cocoa Touch UI Testing Bundle(iOS)
  • Cocoa UI Testing Bundle (OS X)
APIs

Three new classes

  • XCUIApplication
  • XCUIElement
  • XCUIElementQuery
UI Recording

Recording generates the code

  • Create new tests
  • Expand existing tests

控件和查询

Element Uniqueness

Every XCUIElement is backed by a query
Query must resolve to exactly one match

  • No matches or multiple matches cause test failure
  • Failure raised when element resolves query
    Exception
  • Exists property
XCUIElementQuery

API for specifying elements
Queries resolve to collections of accessible elements

  • Number of matches: count
  • Specify by identifier: subscripting
  • Specify by index: elementAtIndex()

How do queries work?

  • Relationships
  • Filtering
Expressing relationships

Descendants
Children
Containment

Filtering
过滤方式 方式类型
Element type Button,table,menu.etc
Identifiers Accessibility identifier,label,title,etc
Predicates Value,partial matching,etc

Combining Relationships and Filtering

descandantsMatchingType()
let allButtons = app.descendantsMatchingType(.Button)

let allCellsInTable = table.descendantsMatchingType(.Cell)

let allMenuItemsInMenu = menu.descendantsMatchingType(.MenuItem)
So common, wo provide convenience API for each type
let allButtons = app.buttons
let allCellsInTables = table.cells
let allMenuItemsInMenu = menu.menuItems
childrenMatchingType()

Differentiates between any descendant and a direct child relationship

let allButtons = app.buttons //descendantsMatchingType(.Button)

let childButtons = navBar.childrenMatchingType(.Button)
containingType()

Find elements by describing their descendants

屏幕快照 2018-02-08 下午1.33.17.png
let cellQuery = cells.containingType(.StaticText, identifier:"Groceries")

descendantsMatchingType()
childrenMatchingType()
containingType()
这三个方法也可以同样的方式使用
Combining Queries

Queries can be "chained" together
Output of each query is the input of the next query

屏幕快照 2018-02-08 下午1.39.28.png
let labelsInTables = app.tables.staticTest
Getting Elements from Queries
方式 示例
Subscripting table.staticTests["Groceries"]
Index table.staticTests.elementAtIndex(0)
Unique app.navigationBars.element

生成截图

我运行完之后,发现没有图片生成


屏幕快照 2018-02-08 下午2.07.27.png

需要去scheme中的text设置


text设置

使用终端测试

xcodebuild test -workspace UITerminalDemo.xcworkspace 
-scheme UITerminalDemoUITests 
-destination 'platform=iOS Simulator,name=iPhone X,OS=11.2' 

下面是destination的可用值:
(也是可以用真机的,下面的第一条就是我自己的手机,需要连接上)

Available destinations for the "UITerminalDemoUITests" scheme:
        { platform:iOS, id:86c38f55392f78e1c14ee5b1e5e547492075df20, name:许龙的 iPhone }
        { platform:iOS Simulator, id:E5186B36-BD50-412D-8AD2-E9A1E1F3AB9C, OS:10.3.1, name:iPad (5th generation) }
        { platform:iOS Simulator, id:9AE98D1F-49F6-4D56-BB17-B5D474941C5D, OS:11.2, name:iPad (5th generation) }
        { platform:iOS Simulator, id:3293AA96-0573-4316-8FC5-7B14559F3E20, OS:10.3.1, name:iPad Air }
        { platform:iOS Simulator, id:44E080B6-B931-42BF-988D-E55345DBF8CB, OS:11.2, name:iPad Air }
        { platform:iOS Simulator, id:288C496A-3AF2-413E-B816-71BE66AE8124, OS:10.3.1, name:iPad Air 2 }
        { platform:iOS Simulator, id:7420C15C-05CC-4C55-AA38-6A5D5E0CCF3B, OS:11.2, name:iPad Air 2 }
        { platform:iOS Simulator, id:21C6851F-2F5E-4F8F-BCEF-51878F70986C, OS:10.3.1, name:iPad Pro (9.7 inch) }
        { platform:iOS Simulator, id:8C328212-6A92-4F42-9DBD-DBB1E040EE08, OS:11.2, name:iPad Pro (9.7-inch) }
        { platform:iOS Simulator, id:2F7428EE-C4AE-4D31-A273-0AE1F88C95E6, OS:10.3.1, name:iPad Pro (10.5-inch) }
        { platform:iOS Simulator, id:D53E887B-2704-4919-9E8E-10E7B0B69DFA, OS:11.2, name:iPad Pro (10.5-inch) }
        { platform:iOS Simulator, id:FC2CCFC2-E298-4334-B66D-9D1E9E3F98E2, OS:10.3.1, name:iPad Pro (12.9 inch) }
        { platform:iOS Simulator, id:B6990DA0-DDA0-4999-AD73-E85D14D1A730, OS:11.2, name:iPad Pro (12.9-inch) }
        { platform:iOS Simulator, id:8AD3E839-1319-4E34-A619-4A1F5C60E202, OS:10.3.1, name:iPad Pro (12.9-inch) (2nd generation) }
        { platform:iOS Simulator, id:82AEFE8B-2DCD-499F-A743-46681C38049A, OS:11.2, name:iPad Pro (12.9-inch) (2nd generation) }
        { platform:iOS Simulator, id:C64ED8D5-F709-4E8F-94FC-B4DB97F906F4, OS:10.3.1, name:iPhone 5 }
        { platform:iOS Simulator, id:8DC1550F-7BD0-443E-8895-9DC074217A60, OS:10.3.1, name:iPhone 5s }
        { platform:iOS Simulator, id:C65A270A-653B-4CC4-AADC-D683D0FEB23A, OS:11.2, name:iPhone 5s }
        { platform:iOS Simulator, id:52DBB418-E842-445A-AB95-398D2D4404CF, OS:10.3.1, name:iPhone 6 }
        { platform:iOS Simulator, id:E3E52BB3-51EA-4E47-A72E-49D281BB8F04, OS:11.2, name:iPhone 6 }
        { platform:iOS Simulator, id:8BEDA513-7394-43BF-8A92-56C47704B5EC, OS:10.3.1, name:iPhone 6 Plus }
        { platform:iOS Simulator, id:45D79B21-EE8D-4C6C-8C68-744D8AB388F5, OS:11.2, name:iPhone 6 Plus }
        { platform:iOS Simulator, id:B1B9D218-C692-4C56-8A6A-E73B81885DB4, OS:10.3.1, name:iPhone 6s }
        { platform:iOS Simulator, id:DBC72166-AC98-4E41-A590-DD76F47EB4BF, OS:11.2, name:iPhone 6s }
        { platform:iOS Simulator, id:21B5972B-5377-431F-8445-0CD00BC66B77, OS:10.3.1, name:iPhone 6s Plus }
        { platform:iOS Simulator, id:FDA53200-DBBD-498A-BB58-2B05B17BD785, OS:11.2, name:iPhone 6s Plus }
        { platform:iOS Simulator, id:33122DBD-44EC-40E1-BE5E-85139F0C5B0F, OS:10.3.1, name:iPhone 7 }
        { platform:iOS Simulator, id:6C2EDE5A-4C66-48C6-844F-9A54873904D4, OS:11.2, name:iPhone 7 }
        { platform:iOS Simulator, id:35112E51-2376-4E21-B49D-34A69FBC8DB7, OS:10.3.1, name:iPhone 7 Plus }
        { platform:iOS Simulator, id:44EB9D0D-9D53-4D34-BEAD-7E8DE3162D2D, OS:11.2, name:iPhone 7 Plus }
        { platform:iOS Simulator, id:09E387B5-C0E6-43D9-9BA8-75E3785280C0, OS:11.2, name:iPhone 8 }
        { platform:iOS Simulator, id:F85B7C01-EB2D-4C9E-98C0-CF466941E6E0, OS:11.2, name:iPhone 8 Plus }
        { platform:iOS Simulator, id:E76E917B-DAFB-4849-BC6E-A2AD8CEDE025, OS:10.3.1, name:iPhone SE }
        { platform:iOS Simulator, id:C3F35392-D7C4-44AF-A9E4-4A79E07B4213, OS:11.2, name:iPhone SE }
        { platform:iOS Simulator, id:2B7D423E-D828-4F13-97F3-A2FE4831BF8D, OS:11.2, name:iPhone X }

    Ineligible destinations for the "UITerminalDemoUITests" scheme:
        { platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Generic iOS Device }
        { platform:iOS Simulator, id:dvtdevice-DVTiOSDeviceSimulatorPlaceholder-iphonesimulator:placeholder, name:Generic iOS Simulator Device }

如果想一次测试多个设备怎么办?
可以使用链式语法指定多个-destination

如果项目使用了Cocoapods的话,也就是打开项目使用的是.workspace的话,需要用 -workspace,如果不是的话需要用-project

xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp
-destination 'platform=OS X,arch=x86_64'
-destination 'platform=iOS,name=Development iPod touch'
-destination 'platform=Simulator,name=iPhone,OS=9.0'

如果测试失败,则会返回一个非零的Code码。如果想了解更多的·xcodebuild·命令信息,可以在终端中使用man xcodebuild

生成截图

无需额外的工作,只需添加derivedDataPath选项,记得在scheme配置中不要勾选Delete when each test succeeds

xcodebuild test -workspace UITerminalDemo.xcworkspace 
-scheme UITerminalDemoUITests 
-destination 'platform=iOS Simulator,name=iPhone X,OS=11.2' 
-derivedDataPath './test' 

截图路径:./test/Logs/Test/Attachments/

截图.png

关于查询

导航栏标题的查询

最开始我一直找不到导航栏的标题的Label,最后发现是查询条件的问题,title的类型不是XCUIElementTypeStaticText,而是XCUIElementTypeOther,我是先通过下面的代码找到的

    XCUIApplication *app = [[XCUIApplication alloc] init];
    NSInteger navigationBarCount = app.navigationBars.count;
    NSLog(@"%ld",navigationBarCount);
    XCUIElement *navigationBar = [app.navigationBars elementBoundByIndex:0];
    XCUIElementQuery *navTitleLabels = [navigationBar descendantsMatchingType:XCUIElementTypeAny];
    NSLog(@"navlabels:%ld",navTitleLabels.count);
    XCUIElement *any = [navTitleLabels elementBoundByIndex:0];
    NSLog(@"description: %@", any.debugDescription);
 description: Attributes: Other, 0x60c0001993d0, traits: 8590000128, {{170.3, 55.7}, {34.7, 20.3}}, label: '首页'

可以发现Attributes是Other,下面直接使用XCUIElementTypeOther类型进行查找

    XCUIApplication *app = [[XCUIApplication alloc] init];
    NSInteger navigationBarCount = app.navigationBars.count;
    NSLog(@"%ld",navigationBarCount);
    XCUIElement *navigationBar = [app.navigationBars elementBoundByIndex:0];
    XCUIElementQuery *navTitleLabels = [navigationBar descendantsMatchingType:XCUIElementTypeOther];
    NSLog(@"navlabels:%ld",navTitleLabels.count);
    XCUIElement *other = [navTitleLabels elementBoundByIndex:0];
    NSLog(@"description: %@", other.debugDescription);

打印信息是一样的

 description: Attributes: Other, 0x60c0001993d0, traits: 8590000128, {{170.3, 55.7}, {34.7, 20.3}}, label: '首页'

所以:要找到NavigationBar的标题(系统的)查询类型是XCUIElementTypeOther。


关于检测

XCTFail(...)

生成一个无条件的错误,参数...是输出的提示文字(后面类似)。

/*!
 * @function XCTFail(...)
 * Generates a failure unconditionally.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTFail(...) \
    _XCTPrimitiveFail(self, __VA_ARGS__)
XCTAssert(expression, ...)

当参数expression是false的时候生成一个错误,参数...同上。

/*!
 * @define XCTAssert(expression, ...)
 * Generates a failure when ((\a expression) == false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssert(expression, ...) \
    _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

通过按住Command+Control,然后点击宏定义,跳进宏定义声明的地方,可查看所有的XCTAssert断言的定义声明。如下

/*!
 * @define XCTAssertNil(expression, ...)
 * Generates a failure when ((\a expression) != nil).
 * @param expression An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNil(expression, ...) \
    _XCTPrimitiveAssertNil(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertNotNil(expression, ...)
 * Generates a failure when ((\a expression) == nil).
 * @param expression An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotNil(expression, ...) \
    _XCTPrimitiveAssertNotNil(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssert(expression, ...)
 * Generates a failure when ((\a expression) == false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssert(expression, ...) \
    _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)
使用断言

比如我要判断当前页面,可以通过导航标题来判断

    NSInteger navigationBarCount = app.navigationBars.count;
    XCUIElement *navigationBar = [app.navigationBars elementBoundByIndex:0];
    XCUIElementQuery *navTitleLabels = [navigationBar descendantsMatchingType:XCUIElementTypeOther];
    XCUIElement *otherNavTitle = [navTitleLabels elementBoundByIndex:0];
    //判断导航标题是不是详情页
    XCTAssert([otherNavTitle.label isEqualToString:@"详情页"]);

关于测试用例

在test.m中默认有一个testExample,如果你想新写一个测试方法,注意方法名需要用text开头。这样才会在方法的左边出现单个方法测试的可点击按钮,如果写完方法没出现,command+U运行下即可。

text.png

点击@implementation UITests左边的运行所有测试用例或者command+u。
发现Xcode运行测试用例是按字母排序的。如果想要按固定顺序执行测试用例,可以在test后追加数字来标记顺序,比如:

- (void)test00TabPage {
  //your test0
}

- (void)test01TabPage {
  //your test1
}

- (void)test02TabPage {
  //your test2
}

参考:

WWDC15 UI Testing in Xcode PDF

Testing with Xcode

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容