什么是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
-
code cases
-
run the target (点那个小三角运行)
如何写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。-
改签名(这里需要用自己的开发者账号)
-
通过 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
看到下面这样就对了:
上面已经实现了在手机上面跑一个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属性。