UI Tests自动化测试

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 和并指定 containingTypedescendants 可以遍历所有子节点搜索。我们可以通过级联和结合使用这些方法获取到我们想要的层级的元素。

查找元素的方法:

  • 遍历查找,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,全依赖工具是无法完成完整测试;

元素查找工具

image

command line test

  1. 在将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测试案例的编写太过于复杂,占用太多人力成本是不利于系统性的测试覆盖的。目前看来,多数案例需要多次调试查找元素及测试才能完成,不大省时。

  1. 输入框设定值之前,需要先为其获取焦点。eg.textfield.tap()

  2. 在测试过程中,需要设置等待时间。实际测试中,存在程序执行的时延,或者是网络请求的时延。有关界面的变动都需要考虑时延的问题,因为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 单元测试文章资源精华
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容