什么是UI自动化测试呢?就是说我们跑一个程序,然后就会看到app跑起来,并且不用我们做操作,它自己会实现各种点击跳转之类的,这样我们就可以把一些标准case转化为代码,每次发包之前跑一次,减少了QA的操作。
1. XCode 7之前的 UI Automation
参考:https://www.jianshu.com/p/0e28ae1bd2c2
这个part不会仔细说,因为现在的Xcode已经没有这个功能了,如果感兴趣可以看上面的文章,大概的操作就是通过instruments里面的Automation,然后输入脚本:

2. XCTest框架
Xcode 8 及以后,苹果将之前的自动化Automation换成了内置的代码target,可以运行起来进行自动化的展示点击之类的。
可以参考:https://www.jianshu.com/p/10795157fdc0
其实挺简单的,主要是以下几个步骤:
-
create UI Testing target
创建target -
code cases
写代码 -
run the target (点那个小三角运行)
run test
如何写UI测试代码
不管是Unit Test Class还是 UI Test Class 都继承自XCTestCase。Test Class中编写的测试方法都要以test开头,否则的话不会执行。因此,如果想暂时关闭测试方法,可以再方法名前面加一个Disable前缀,简洁明了,func DISABLE_testAddition() 这样。下图是一个Test Method 实现:
- (void)testButtonClick {
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
[app.buttons[@"ScrollTestViewController"] tap];
NSLog(@"ScrollTestViewController tapped");
sleep(10);
}
如果我们打开Test Class 会看到每个测试类都回重写setUp()、tearDown()方法。
其实所有的test方法是异步执行的,并且在执行之前都会调用setUp()方法,在执行之后都会调用tearDown()方法。
因此有些重复的公有代码,可以放在setUp()、tearDown()方法中, 比如启动程序,设置公有的测试模拟数据等。在测试的执行代码运行后,通常会使用XCAssert来断言运行结果,XCAssert会很明显的呈现测试的结果,一旦设置的断言条件不通过,就会中断并显示结果,有助于我们的定位问题。
XCTest框架briefing
这一部分只是一些简要的东西,具体内容还是要看官方文档。XCTest Framework是 UITest 实现的关键,主要由XCUIApplication,XCUIElement,XCUIElementQuery三个类以及XCUIElementAttributes,XCUIElementTypeQueryProvider两个协议构成。
XCUIApplication类继承自XCUIElement类,XCUIElement类遵循XCUIElementAttributes和XCUIElementTypeQueryProvider协议,而XCUIElementTypeQueryProvider协议返回的UI元素对象则是XCUIElementQuery类。
XCUIApplication实现application的launch、 terminal、 active、 state等功能, XCUIElement定义元素的操作事件,XCUIElementTypeQueryProvider协议实现查询所有的UI元素对象,XCUIElementAttributes则表示元素的属性,XCUIElementQuery表示元素的一些功能等等。
来看一个UI Test的实现,通过XCUIElementTypeQueryProvider获取到UI 元素对象,并且向对象发送事件。
class CalcUITests: XCTestCase {
let app = XCUIApplication() //获取XCUIApplication对象
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch() //XCUIApplication加载
}
override func tearDown() {
super.tearDown()
}
func testAddition() {
//查询到app的元素,并发送事件
app.buttons["6"].tap()
app.buttons["+"].tap()
app.buttons["2"].tap()
app.buttons["="].tap()
if let textFieldValue = app.textFields["display"].value as? String {
XCTAssertTrue(textFieldValue == "8", "Part 1 failed.")
}
}
我们能从element上面获取哪些信息呢,其实就是XCUIElementAttributes的内容:
@protocol XCUIElementAttributes
/*! The accessibility identifier. */
@property (readonly) NSString *identifier;
/*! The frame of the element in the screen coordinate space. */
@property (readonly) CGRect frame;
/*! The raw value attribute of the element. Depending on the element, the actual type can vary. */
@property (readonly, nullable) id value;
/*! The title attribute of the element. */
@property (readonly, copy) NSString *title;
/*! The label attribute of the element. */
@property (readonly, copy) NSString *label;
/*! The type of the element. /seealso XCUIElementType. */
@property (readonly) XCUIElementType elementType;
/*! Whether or not the element is enabled for user interaction. */
@property (readonly, getter = isEnabled) BOOL enabled;
/*! The horizontal size class of the element. */
@property (readonly) XCUIUserInterfaceSizeClass horizontalSizeClass;
/*! The vertical size class of the element. */
@property (readonly) XCUIUserInterfaceSizeClass verticalSizeClass;
/*! The value that is displayed when the element has no value. */
@property (readonly, nullable) NSString *placeholderValue;
/*! Whether or not the element is selected. */
@property (readonly, getter = isSelected) BOOL selected;
#if TARGET_OS_TV
/*! Whether or not the elment has UI focus. */
@property (readonly) BOOL hasFocus;
#endif
@end
我们能从app里面拿到哪些东西呢,其实就是XCUIElementTypeQueryProvider里面的内容,例如buttons:
@protocol XCUIElementTypeQueryProvider
@property (readonly, copy) XCUIElementQuery *touchBars;
@property (readonly, copy) XCUIElementQuery *groups;
@property (readonly, copy) XCUIElementQuery *windows;
@property (readonly, copy) XCUIElementQuery *sheets;
@property (readonly, copy) XCUIElementQuery *drawers;
@property (readonly, copy) XCUIElementQuery *alerts;
@property (readonly, copy) XCUIElementQuery *dialogs;
@property (readonly, copy) XCUIElementQuery *buttons;
@property (readonly, copy) XCUIElementQuery *radioButtons;
@property (readonly, copy) XCUIElementQuery *radioGroups;
@property (readonly, copy) XCUIElementQuery *checkBoxes;
@property (readonly, copy) XCUIElementQuery *disclosureTriangles;
@property (readonly, copy) XCUIElementQuery *popUpButtons;
@property (readonly, copy) XCUIElementQuery *comboBoxes;
@property (readonly, copy) XCUIElementQuery *menuButtons;
@property (readonly, copy) XCUIElementQuery *toolbarButtons;
@property (readonly, copy) XCUIElementQuery *popovers;
@property (readonly, copy) XCUIElementQuery *keyboards;
@property (readonly, copy) XCUIElementQuery *keys;
@property (readonly, copy) XCUIElementQuery *navigationBars;
@property (readonly, copy) XCUIElementQuery *tabBars;
@property (readonly, copy) XCUIElementQuery *tabGroups;
@property (readonly, copy) XCUIElementQuery *toolbars;
@property (readonly, copy) XCUIElementQuery *statusBars;
@property (readonly, copy) XCUIElementQuery *tables;
@property (readonly, copy) XCUIElementQuery *tableRows;
@property (readonly, copy) XCUIElementQuery *tableColumns;
@property (readonly, copy) XCUIElementQuery *outlines;
@property (readonly, copy) XCUIElementQuery *outlineRows;
@property (readonly, copy) XCUIElementQuery *disclosedChildRows;
@property (readonly, copy) XCUIElementQuery *browsers;
@property (readonly, copy) XCUIElementQuery *collectionViews;
@property (readonly, copy) XCUIElementQuery *sliders;
@property (readonly, copy) XCUIElementQuery *pageIndicators;
@property (readonly, copy) XCUIElementQuery *progressIndicators;
@property (readonly, copy) XCUIElementQuery *activityIndicators;
@property (readonly, copy) XCUIElementQuery *segmentedControls;
@property (readonly, copy) XCUIElementQuery *pickers;
@property (readonly, copy) XCUIElementQuery *pickerWheels;
@property (readonly, copy) XCUIElementQuery *switches;
@property (readonly, copy) XCUIElementQuery *toggles;
@property (readonly, copy) XCUIElementQuery *links;
@property (readonly, copy) XCUIElementQuery *images;
@property (readonly, copy) XCUIElementQuery *icons;
@property (readonly, copy) XCUIElementQuery *searchFields;
@property (readonly, copy) XCUIElementQuery *scrollViews;
@property (readonly, copy) XCUIElementQuery *scrollBars;
@property (readonly, copy) XCUIElementQuery *staticTexts;
@property (readonly, copy) XCUIElementQuery *textFields;
@property (readonly, copy) XCUIElementQuery *secureTextFields;
@property (readonly, copy) XCUIElementQuery *datePickers;
@property (readonly, copy) XCUIElementQuery *textViews;
@property (readonly, copy) XCUIElementQuery *menus;
@property (readonly, copy) XCUIElementQuery *menuItems;
@property (readonly, copy) XCUIElementQuery *menuBars;
@property (readonly, copy) XCUIElementQuery *menuBarItems;
@property (readonly, copy) XCUIElementQuery *maps;
@property (readonly, copy) XCUIElementQuery *webViews;
@property (readonly, copy) XCUIElementQuery *steppers;
@property (readonly, copy) XCUIElementQuery *incrementArrows;
@property (readonly, copy) XCUIElementQuery *decrementArrows;
@property (readonly, copy) XCUIElementQuery *tabs;
@property (readonly, copy) XCUIElementQuery *timelines;
@property (readonly, copy) XCUIElementQuery *ratingIndicators;
@property (readonly, copy) XCUIElementQuery *valueIndicators;
@property (readonly, copy) XCUIElementQuery *splitGroups;
@property (readonly, copy) XCUIElementQuery *splitters;
@property (readonly, copy) XCUIElementQuery *relevanceIndicators;
@property (readonly, copy) XCUIElementQuery *colorWells;
@property (readonly, copy) XCUIElementQuery *helpTags;
@property (readonly, copy) XCUIElementQuery *mattes;
@property (readonly, copy) XCUIElementQuery *dockItems;
@property (readonly, copy) XCUIElementQuery *rulers;
@property (readonly, copy) XCUIElementQuery *rulerMarkers;
@property (readonly, copy) XCUIElementQuery *grids;
@property (readonly, copy) XCUIElementQuery *levelIndicators;
@property (readonly, copy) XCUIElementQuery *cells;
@property (readonly, copy) XCUIElementQuery *layoutAreas;
@property (readonly, copy) XCUIElementQuery *layoutItems;
@property (readonly, copy) XCUIElementQuery *handles;
@property (readonly, copy) XCUIElementQuery *otherElements;
@property (readonly, copy) XCUIElementQuery *statusItems;
/*!
* Returns an element that will use the query for resolution. This changes how the query is resolved
* at runtime; instead of evaluating against every element in the user interface, `firstMatch` stops
* the search as soon as a single matching element is found. This can result in significantly faster
* evaluation if the element is located early in a large view hierarchy but also means that multiple
* matches will not be detected.
*/
@property (readonly) XCUIElement *firstMatch;
@end
我们可以对element做的操作可以看XCUIElement,因为比较多所以就不都贴出来啦,看几个叭:
/*!
* Moves the cursor over the element.
*/
- (void)hover;
/*!
* Sends a click event to a hittable point computed for the element.
*/
- (void)click;
/*!
* Sends a double click event to a hittable point computed for the element.
*/
- (void)doubleClick;
/*!
* Sends a right click event to a hittable point computed for the element.
*/
- (void)rightClick;
单元测试和 UI 测试的区别
对于Unit Test 和 UI Test,两者实现的方式是截然不同的。单元测试的实现方式是保证测试目标类的访问权限,实例化这个类 或者 模拟这个类 (使用类似 OCMock的方式 ),然后在测试方法中调用这个类需测试的方法、功能。
而对 UI Test,则依靠XCFramework实现。利用XCUIApplication、 XCUIElement 、XCUIElementQuery来获取的App的各个UI对象,同步事件等等,并且将触发事件发送给UI对象。
Unit Test 会访问目标类的内部方法,而UI Test则不会,只会在外部触发事件。
集成
我们编写测试代码的最终目的其实是为了方便测试和修改,这样的话测试自动化即持续集成会带来很多益处。即时发现代码问题、保证主干版本质量、节约测试时间成本等等。因此将Test持续集成到CI工具是很好的做法。Xcode可以很好的和OS X Server结合使用,关联到 Git 版本库之后创建包含 Test 的bot,持续构建 测试 和 部署。
当然了你还可以使用xcodebuild命令行工具编写自动构建、测试脚本,结合Fastlane 和 Jenkins ,实现自动化测试。
3. 远程控制测试(电脑控制手机自动跑case但不用xcode)
这一块主要是通过手机端的Python代码远程遥控手机的自动测试,主要是借用wda框架。
WebDriverAgent briefing
可以参考:https://www.jianshu.com/p/b2007f520c77
首先是这个框架是干啥的?WDA(WebDriverAgent)是Facebook推出的一个开源ios自动化测试框架。(官方链接:https://github.com/facebookarchive/WebDriverAgent)
你可以通过mac的 xcode WebDriverAgent 工程 run起一个wda的程序在手机上,然后手机就成为了一个server,电脑就可以通过端口控制手机跑自动测试(自动测试仍旧是借助于XCTest的框架哒),server也就是手机再通过调用XCTest.framework和调用苹果的API直接在设备上执行命令。
WebDriverAgent setup
这部分会分为两个part,在手机上run起wda的工程 & 编写Python程序来调动手机的自动测试。
(1)在手机上run起wda的工程
- 安装各种wda依赖的库
① brew install carthage
② brew install node
③ brew install --HEAD libimobiledevice
④ npm install -g iproxy
- 下载wda源码
git clone https://github.com/appium/WebDriverAgent
初始化
进入wda源码下载的路径,执行 ./Scripts/bootstrap.sh。-
改签名(这里需要用自己的开发者账号)
改sign

-
通过 Product->Test 跑到手机上面
跑到手机上面 如果成功的话你会在xcode的console里面看到这些:
2020-09-07 14:01:50.959728+0800 WebDriverAgentRunner-Runner[1387:217902] Built at Sep 7 2020 11:11:06
2020-09-07 14:01:50.983945+0800 WebDriverAgentRunner-Runner[1387:217902] ServerURLHere->http://10.0.240.98:8100<-ServerURLHere
2020-09-07 14:01:50.984597+0800 WebDriverAgentRunner-Runner[1387:218027] Using singleton test manager
- 然后连接看一下,通过电脑打开
http://localhost:8100/status看到下面这样就对了:
status
上面已经实现了在手机上面跑一个wda的程序,下面我们来连接这个手机的server做一些事情。
- 手机上面的server不能通过外部连接,所以需要用
iproxy转换一下,注意这里的端口号其实就是上面命令行打出来的:
命令行输入:iproxy 8100 8100
输出:Creating listening port 8100 for device port 8100
waiting for connection
这里的waiting其实是手机在waiting我们的连接
- 安装python库
pip install facebook-wda
- 新建个python文件写代码
import wda
def test():
c = wda.Client('http://localhost:8100')
print(c.status())
c.screenshot('screen.png')
c.session('app bundle id')
# c.lock()
# c.unlock()
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
test()
然后只要你的xcode还在运行wda的runner,你跑起来这个py文件,就会发现自动截屏了~ 但是截屏会存在你的电脑上面:

- 更多关于wda的指令可以看这个:https://github.com/openatx/facebook-wda
附录:如何识别页面元素(Accessibility Inspector)

你选中自己的手机,然后点右侧的小定位按钮,等待它变为蓝色以后,点击手机上面,会发现有绿色的小方块,也就是你选中了哪一个,Accessibility Inspector正在展示的就是哪个元素的信息:

你就可以通过元素的 title 或者 identifier 拿到元素做操作啦~ 例如:
s(id="URL").exists # return True or False
# using id or other query conditions
s(id='URL')
# using className
s(className="Button")
# using name
s(name='URL')
s(nameContains='UR')
s(nameMatches=".RL")
# using label
s(label="label")
s(labelContains="URL")
# using value
s(value="Enter")
s(valueContains="RL")
- 然而,有些时候我们点击app中的UI组件的时候,会出现点击的组件无法定位或者整个手机屏幕都变绿的情况,这时候我们可以尝试两种解决方法:
点击inspector中的左移或者右移按钮,这时候会发现app中绿色的位置在移动,继续点击移动到想要选中的组件即可。
若使用第一种方法依然无法选中想点击的组件,可能是组件之间的层次关系导致的问题。当前inspector绿色色块所在的UI层级,跟想要点击的UI组件不是同一个层级,所以无论如何移动都无法移动到想要点击的组件。
但我们观察inspector工具界面,可以发现界面下半部分可以显示出UI层次结构,并且我们可以点击箭头进入更下级的UI层次
Hierarchy,然后我们继续点击inspector的左右移动按钮,就可以选中目标UI组件特别说明,有时候已经选中目标UI组件,却发现它的Accessibility属性都是空的,这时候就没办法利用Accessibility属性来定位UI组件了,一个可能的办法是可以让开发同学给目标UI组件加上Accessibility属性。





