UI Tests
是一个自动测试UI与交互的Testing组件,它可以通过编写代码、或者是记录开发者的操作过程并代码化,来实现自动点击某个按钮、视图,或者自动输入文字等功能。会在基于宿主App创建一个App,用于模拟测试UI。
测试顺序
在新增UITests Target后,根据业务需要可能存在多个测试文件,每个文件存在多个模块测试方法。
顺序如下:
多文件时,文件中定义的类名的英文排序就是执行顺序;
单个文件中多个测试方法,根据方法名称的字母排序顺序执行;
Tip:因此可以将“翻阅启动页”的动作放到第一个执行的test中.
框架方法
工欲善其事必先利其器
Application
XCUIApplication
这个类继承自XCUIElement
,掌管应用程序的生命周期。它有下面几个方法和属性。
open class XCUIApplication : XCUIElement {
public init()
public init(bundleIdentifier: String)
//启动App
open func launch()
//唤醒App
open func activate()
//终止app
open func terminate()
//指定启动参数,宿主app通过 ProcessInfo().arguments.contains("String")可以访问
open var launchArguments: [String]
//指定启动环境变量
open var launchEnvironment: [String : String]
//当前app状态
open var state: XCUIApplication.State { get }
//等待app进入某种状态,timeout后放弃等待
open func wait(for state: XCUIApplication.State, timeout: TimeInterval) -> Bool
}
ElementQuery
XCUIElementQuery
可以表示为元素查找集。通过集合细分查找元素
open class XCUIElementQuery : NSObject, XCUIElementTypeQueryProvider {
//当前元素集合
open var element: XCUIElement { get }
//集合内的元素个数
open var count: Int { get }
//根据索引读取元素
@available(iOS, introduced: 9.0, deprecated: 9.0, message: "Use elementBoundByIndex instead.")
open func element(at index: Int) -> XCUIElement
//根据索引读取元素
open func element(boundBy index: Int) -> XCUIElement
//根据NSPredicate检索元素
open func element(matching predicate: NSPredicate) -> XCUIElement
open func element(matching elementType: XCUIElement.ElementType, identifier: String?) -> XCUIElement
open subscript(key: String) -> XCUIElement { get }
open var allElementsBoundByAccessibilityElement: [XCUIElement] { get }
open var allElementsBoundByIndex: [XCUIElement] { get }
open func descendants(matching type: XCUIElement.ElementType) -> XCUIElementQuery
open func children(matching type: XCUIElement.ElementType) -> XCUIElementQuery
open func matching(_ predicate: NSPredicate) -> XCUIElementQuery
open func matching(_ elementType: XCUIElement.ElementType, identifier: String?) -> XCUIElementQuery
open func matching(identifier: String) -> XCUIElementQuery
open func containing(_ predicate: NSPredicate) -> XCUIElementQuery
open func containing(_ elementType: XCUIElement.ElementType, identifier: String?) -> XCUIElementQuery
open var debugDescription: String { get }
}
query扩展了很多属性
@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 *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
这些属性可以让检索代码更简洁,
app.buttons["login"]
是 app.descendants(matching: .button)
的简写。
还有仅获取当前层级子元素的 children
和并指定 containingType
。descendants
可以遍历所有子节点搜索。我们可以通过级联和结合使用这些方法获取到我们想要的层级的元素。
查找元素的方法:
- 遍历查找,
app.buttons["xxx"]
会先遍历子节点找出所有button,然后通过关键字xxx
找到具体元素。关键字可以是title,或者是 Accessibility identifier。
- 通过层级去获取元素
app.otherElements.cells.firstMatch.otherElements.buttons.element(boundBy: 0)
层级的查找还可以借助辅助录屏工具。(继续阅读)
app.buttons["登录"] //找出所有button,从中检索title="登录"或者Accessibility identifier=“登录”的按钮
app.cells.firstMatch.buttons["找相似"]//找出所有cells,取出第一个cell;找出所有buttons,寻找匹配的
Element
XCUIElement
可以表示系统的各种UI元素。
open class XCUIElement : NSObject, XCUIElementAttributes, XCUIElementTypeQueryProvider {
//元素是否存在
open var exists: Bool { get }
//等待元素出现,timeout后放弃等待
open func waitForExistence(timeout: TimeInterval) -> Bool
//view hitTest的执行结果
open var isHittable: Bool { get }
//获取当前element下
open func descendants(matching type: XCUIElement.ElementType) -> XCUIElementQuery
//
open func children(matching type: XCUIElement.ElementType) -> XCUIElementQuery
//获取元素在屏幕中的坐标信息
open func coordinate(withNormalizedOffset normalizedOffset: CGVector) -> XCUICoordinate
// po app = po print(app.debugDescription) 可以打印当前UI所有subtree结构。
// po print(element.debugDescription) 可以打印element的查找路径。
// 结合app subtree结构 和 元素查找路径 可以找出并定位任一元素的信息
open var debugDescription: String { get }
}
Element Attribute
通过一系列检索操作,终于找到目标元素。那么可以对元素做一些什么事情呢?可以读取元素的属性用于逻辑判断
public protocol XCUIElementAttributes {
public var identifier: String { get }
public var frame: CGRect { get }
public var value: Any? { get }
public var title: String { get }
public var label: String { get }
@available(iOS 9.0, *)
public var elementType: XCUIElement.ElementType { get }
public var isEnabled: Bool { get }
@available(iOS 9.0, *)
public var horizontalSizeClass: XCUIElement.SizeClass { get }
@available(iOS 9.0, *)
public var verticalSizeClass: XCUIElement.SizeClass { get }
public var placeholderValue: String? { get }
public var isSelected: Bool { get }
}
对元素的一系列操作,比如连续点击100下、旋转、长按等,对于人力测试来说难度很大的测试工作
extension XCUIElement {
open func tap()
open func doubleTap()
open func twoFingerTap()
open func tap(withNumberOfTaps numberOfTaps: Int, numberOfTouches: Int)
open func press(forDuration duration: TimeInterval)
open func press(forDuration duration: TimeInterval, thenDragTo otherElement: XCUIElement)
open func swipeUp()
open func swipeDown()
open func swipeLeft()
open func swipeRight()
open func pinch(withScale scale: CGFloat, velocity: CGFloat)
open func rotate(_ rotation: CGFloat, withVelocity velocity: CGFloat)
}
当然还可以模拟托拽:
//拖拽一定距离
var x = 0
let cate = app.scrollViews.firstMatch //分类collectionView
for _ in 0...2 {
let start = cate.coordinate(withNormalizedOffset: CGVector.init(dx: x, dy: 0))
x-=10 //托拽的方向位移
let end = cate.coordinate(withNormalizedOffset: CGVector.init(dx: x, dy: 0))
start.press(forDuration: 0.1, thenDragTo: end)
}
Tests工具
屏幕录制工具
懒人方式之用于探测性的获取指定元素。但是有些元素无法获取,需要设置Accessibility 中的identifier,相当于UITest中元素的ID。
优点:所见即所得;缺点:多数元素无法捕获,因为没有设置Accessibility identifier,全依赖工具是无法完成完整测试;
元素查找工具
command line test
- 在将UI Tests集成进Gitlab CI时,需要先统一测试环境,即卸载模拟器上的宿主app,clean缓存,执行测试。对于mac上模拟器的操作可以通过指令xcrun 和 instruments。可以研究一下这些命令玩转模拟器
xcrun instruments -w list 列出所有设备
...
iPhone X (11.2) [DD7A60C1-3F5A-4172-99D4-54ECC179E6E2] (Simulator)
iPhone X (11.3) [4C0397C5-5511-4576-A93A-4E7452A014E0] (Simulator)
启动指定模拟器
xcrun instruments -w 4C0397C5-5511-4576-A93A-4E7452A014E0
唤起app
xcrun simctl launch booted com.lukou.youxuan
卸载app
xcrun simctl uninstall 4C0397C5-5511-4576-A93A-4E7452A014E0 com.lukou.youxuan
开始执行测试
xcodebuild -workspace panpan.xcworkspace -scheme panpanUITests -configuration Debug -destination 'platform=iOS Simulator,OS=11.3,name=iPhone X' clean test
程序中区分UITests环境
启动宿主app测试时,根据需要可以添加标识来区分是否正在进行UITest。
extension XCUIApplication {
static func launchApp() {
let app = XCUIApplication()
app.launchArguments = ["testMode"]
app.launch()
}
}
//App
extension UIApplication {
public static var isRunningTest: Bool {
return ProcessInfo().arguments.contains("testMode")
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if !UIApplication.isRunningTest {
print("UI Testing ")
}
}
测试外部App
可启动当前模拟器上已安装app用于测试。需指定bundleId
let app = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
app.launch() _ = app.wait(for: .runningForeground, timeout: 30)
调试技巧
在编写测试用例时,需要查找元素,执行一系列动作,获取元素并断言。根据上述的查找方式,调试起来其实很麻烦。经过多次尝试,找到了一个非常简便的测试办法。首先断点进入任意测试方法内部,然后可在lldb中:
po app //查看当前view所有元素结构
po app.buttons["abc"] //查找你想要的元素
po app.buttons.firstMath.tap()
//所执行的动作会即可作用在当前模拟器中,你也可以直接操作模拟器去展现你要的view,在通过lldb去查找元素,调用动作。
相当于通过lldb,你可以动态执行测试案例。可以任意操作app,去寻找你想要的元素和动作
总结
UI自动化测试固然是好,但当UITests测试案例的编写太过于复杂,占用太多人力成本是不利于系统性的测试覆盖的。目前看来,多数案例需要多次调试查找元素及测试才能完成,不大省时。
输入框设定值之前,需要先为其获取焦点。eg.textfield.tap()
在测试过程中,需要设置等待时间。实际测试中,存在程序执行的时延,或者是网络请求的时延。有关界面的变动都需要考虑时延的问题,因为uitest的执行速度比实际程序运行更直接。比如应用启动后,测试点击首页tab。在app.launch后,需要设置等待tab出现后,才能测试tab.tap()。
func testLogin() {
let app = XCUIApplication()
_ = app.tabBars.buttons["我的"].waitForExistence(timeout: 10)
app.tabBars.buttons["我的"].tap()
_ = app.tables.buttons["点击登录"].waitForExistence(timeout: 10)
let btns = app.tables.buttons["点击登录"]
XCTAssert(btns.exists, "用户已登录")
btns.firstMatch.tap()
let phoneTF = app.textFields["请输入手机号码"]
phoneTF.tap()
phoneTF.typeText("1708618**01")
let codeTF = app.textFields["请输入验证码"]
codeTF.tap()
codeTF.typeText("7521")
app.buttons["快速登录"].tap()
expectation(for: NSPredicate(format: "exists == 1"), evaluatedWith: app.tables.buttons["170****7601"]) { () -> Bool in
print("find button! ")
return true //认为已经找到
//return false //认为未找到
}
//同步执行,查找并等待直到timeout
waitForExpectations(timeout: 5) { (error) in
print("cant find button. error:\(error)")
}
}
或者等待按钮出现(同步执行),waitForExistence会每秒1次检测元素是否存在,直到timeout秒。
_ = app.tables.buttons["170****7601"].waitForExistence(timeout: 10)
let success = app.tables.buttons["170****7601"].exists
XCTAssert(success, "登录失败")
3.想要访问App宿主app内的代码,需要先修改设置:Build Setting -- Deines Moduls=YES;才能import panpan ,访问panpan内部的代码。
更多
在掌握UITests的基本能力之后,可以更多尝试去了解一些不错的三方框架。
Quick ,是建立在XCTest上的框架,更专注业务
Nimble,更明确的断言方式。并且,出错的时候,提示信息会带着上下文的值信息,让开发者更容易的找到错误。
KIF的全称是Keep it functional。它是一个建立在XCTest的UI测试框架。简洁易用
appium采用了Client Server的模式。对于App来说就是一个Serve,然后测试代码通过HTTP请求的方式,来进行实际的测试。跨平台,支持iOS,Android,但是自定义控件支持不好,WebView的支持不好
xctool ,基于xcodebuild命令的扩展,支持-parallelize并行测试多个bundle,大大提高测试效率
最后倾情奉上测试快捷键:
ctrl+option+cmd+u 单元测试。焦点在单个方法内,则测试单个方法。在方法外,则测试整个文件
参考资料
- 2017 Swift 单元测试文章资源精华