iOS 自动化测试(XCTest, UITests)

前言

最近正在学习 iOS 自动化测试(系统自带的 XCTest), 就总结一下自动化测试一些知识.
内容可能有点啰嗦, 如想直接看代码, 可直接跳到最后一栏, 去下载项目

如何创建自动化测试

这里分两种创建情况

  1. 刚新建项目

创建项目时, 勾上 Include UI Tests 即可


iOS 自动化测试(XCTest, UITests)
  1. 已有了项目,却没有自动化测试

其实就是添加 target
我们点击 '+' 号,

在这里插入图片描述

选择 UI Testing Bundle

在这里插入图片描述

创建好的自动化测试在哪里?

创建好之后, 是在该目录下(看图), 默认名称是 项目名 + UITests
看到这里, 写过 app extension 的老哥应该都明白了, 其实这个自动化测试就是一个 app extension 来的, 我们可以随意删除和创建, 甚至创建多个都是没问题的

在这里插入图片描述

如何使用自动化测试

这里一般有两种使用法

  1. 在 Show the Test Navigator 中使用

如图, 放鼠标到, 放在函数上, 例如 testExample()
这时我们就能看到右边有个播放的小箭头, 点击这个小箭头, 我们就开始运行这个 testExample() 函数了
如果运行大写 T(XQUITestDemoUITests), 就是运行这个测试模块中所有函数的意思
运行自动化测试, 和运行项目一样的, 可以先选择设备, 就是选择真机或者某个模拟器

刚创建时, 只有 testLaunchPerformance() 和 testExample() 两个函数
其他函数是我后面写的

在这里插入图片描述
  1. 在 UITest 模块的 .swift 文件里面选择运行

上面的 testExample() 这些函数, 其实就是关联这个文件里面的函数
我们点开 UITest 模块的 .swift 文件(如下图), 就一切明了了

在这里插入图片描述

代码行数那里的四边形, 点击效果和上面说的一样, 就是运行自动化测试
点击 class 旁边的运行, 是运行整个类里面所有函数的(和上面说的运行整个测试模块一样)

注意! 能单独执行的函数一定要 test 开头(就是旁边有四边形的函数)
就比如 testA() 这个就是可以的. methodA() 旁边就没有四边形.

生命周期(运行流程)

周期如下

  1. setUp()
  2. 自定义执行的函数
  3. tearDown()

这里有个点, 要注意一下, 比如你当前测试模块里面有 testA(), testB() 两个函数.
然后你直接点击运行整个测试模块, 他执行的顺序是.

  1. setUp()
  2. testA()
  3. tearDown()

然后再执行

  1. setUp()
  2. testB()
  3. tearDown()

就是你会看到APP启动和关闭了两次.

具体使用介绍

初始化 App

// 初始化 XCUIApplication
let app = XCUIApplication()
// 启动app 
app.launch()

// 默认不填 bundleIdentifier, 就会初始化当前项目APP
// 如果是想搞其他APP, 可传入 bundleIdentifier 初始化, 就可获得其实例
let sefariApp = XCUIApplication.init(bundleIdentifier: "com.apple.mobilesafari")

注意, 以下文章出现 app 的代码, 都是指代 let app = XCUIApplication() 这个

获取元素

这里我就举例几种常用的就行, 其他的, 大家可自行研究

根据类型取元素

可直接查看系统 XCUIElementTypeQueryProvider


在这里插入图片描述

比如这样就能取得该 app 下面所有的 button

app.buttons

但取到 XCUIElementQuery, 还不能直接用
这个时候我们取到具体某个元素才行

为了比较好明白, 这里我举一个例子, UI是这样

在这里插入图片描述

以下是 print(app.debugDescription) 输出的数据
这里再啰嗦一下, debugDescription 属性对于获取元素层级来说挺舒服的, 我们要记得常用 debugDescription

 →Application, 0x2819e7720, pid: 4843, label: 'XQUITestDemo'
    Window (Main), 0x2819e78e0, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x2819e79c0, {{0.0, 0.0}, {375.0, 667.0}}
        Other, 0x2819e7aa0, {{0.0, 0.0}, {375.0, 667.0}}
          Other, 0x2819e7b80, {{0.0, 0.0}, {375.0, 667.0}}
            Other, 0x2819e7c60, {{0.0, 0.0}, {375.0, 667.0}}
              Other, 0x2819e7d40, {{0.0, 0.0}, {375.0, 667.0}}
                Other, 0x2819e7e20, {{0.0, 0.0}, {375.0, 667.0}}
                  NavigationBar, 0x2819e7f00, {{0.0, 20.0}, {375.0, 44.0}}, identifier: '我的'
                    StaticText, 0x2819e8000, {{170.0, 32.0}, {35.0, 20.5}}, label: '我的'
                  Other, 0x2819e80e0, {{0.0, 0.0}, {375.0, 667.0}}
                    Other, 0x2819e81c0, {{0.0, 0.0}, {375.0, 667.0}}
                      Other, 0x2819e82a0, {{0.0, 64.0}, {375.0, 603.0}}
                        Button, 0x280c8eae0, {{30.0, 214.0}, {60.0, 60.0}}, identifier: 'touchMe', label: '点我'
                          StaticText, 0x2819e8460, {{41.5, 233.0}, {37.0, 22.0}}, label: '点我'
            TabBar, 0x2819e8540, {{0.0, 618.0}, {375.0, 49.0}}
              Button, 0x2819e8620, {{2.0, 619.0}, {184.0, 48.0}}, label: '首页'
              Button, 0x2819e1b20, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的', Selected
      Other, 0x2819e1a40, {{0.0, 0.0}, {375.0, 667.0}}, Disabled
        Other, 0x2819e0b60, {{0.0, 0.0}, {375.0, 667.0}}, identifier: 'SVProgressHUD'
    Window, 0x2819e1ce0, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x2819e1f80, {{0.0, 0.0}, {375.0, 667.0}}
        Other, 0x2819e2060, {{0.0, 0.0}, {375.0, 667.0}}

注意, Other 对应的是 UIView

我们现在要点击 tabbar 首页这个按钮, 跳转到首页
那么该怎么做呢? 很简单, 代码就两句

// 取元素
let homePageBtn = app.tabBars.buttons["首页"]
// 点击元素
homePageBtn.tap()

这里我解析一下第一句取元素

app.tabBars 取到的是以下数据

TabBar, 0x2835c3800, {{0.0, 618.0}, {375.0, 49.0}}
    Button, 0x2835c38e0, {{2.0, 619.0}, {184.0, 48.0}}, label: '首页'
    Button, 0x2835c39c0, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的', Selected

然后 app.tabBars.buttons 取到的是

Button, 0x2835c38e0, {{2.0, 619.0}, {184.0, 48.0}}, label: '首页'
Button, 0x2835c39c0, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的', Selected

这个时候, 我们根据类型取元素, 就已经取到最后了
想取首页按钮的话, 有几种方法, 请继续往下看

根据 label 取元素

根据 label 来获取 button

app.tabBars.buttons["首页"]

当然, 按照当前这个UI, 最简单就是 app.buttons["首页"]
但是为了能够更准确的取到某个元素, 最好不要吝啬一点代码

根据下标取元素

因为打印的数据, 里面的元素是有序的. 那么我们也可以通过下标来获取元素

// 传入具体的下标, 取出按钮
app.tabBars.buttons.element(boundBy: 0)

// 当然, 也有类似数组一样取法, 取第一个
app.windows.tabBars.firstMatch

根据 identifier 取元素

不过你要提前在项目的代码, 或者 xib 中设置好 identifier 才行

// 这里取的不是 tabbar 的 首页按钮, 是那个黄色按钮
app.tabBars.element(matching: .button, identifier: "touchMe")
  • 在代码中设置

accessibilityIdentifier 就是设置自动化测试时的 identifier

let btn = UIButton()
btn.frame = CGRect.init(x: 30, y: 150, width: 60, height: 60)
btn.setTitle("点我", for: .normal)
btn.backgroundColor = UIColor.orange
btn.accessibilityIdentifier = "touchMe"
  • xib 或者 storyboard 中设置
在这里插入图片描述

对元素的操作

以下 button 就代表是一个按钮元素( XCUIElement类 )

点击

button.tap()

双击

button.doubleTap()

长按

// 长按三秒
button.press(forDuration: 3)

滑动

// 上扫
button.swipeUp()

// 下扫
button.swipeDown()

// 左扫
button.swipeLeft()

// 右扫
button.swipeRight()

捏合

button.pinch(withScale: 1.5, velocity: 1)

旋转

button.rotate(0.5, withVelocity: 1)

实际用例

这里举例一些我学习的时候, 搜了挺久, 都没搜到的实际用例吧
这些用例我都放在了 项目 里面,有兴趣的,可直接去下载 项目 运行一下

具体点击 UITableView 某一行

示例 UI 如下

在这里插入图片描述

打印 app.debugDescription 数据如下

 →Application, 0x2838da060, pid: 1007, label: 'XQUITestDemo'
    Window (Main), 0x2838db560, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x2838db640, {{0.0, 0.0}, {375.0, 667.0}}
        Other, 0x2838db720, {{0.0, 0.0}, {375.0, 667.0}}
          Other, 0x2838db800, {{0.0, 0.0}, {375.0, 667.0}}
            Other, 0x2838db8e0, {{0.0, 0.0}, {375.0, 667.0}}
              Other, 0x2838db9c0, {{0.0, 0.0}, {375.0, 667.0}}
                Other, 0x2838dbaa0, {{0.0, 0.0}, {375.0, 667.0}}
                  NavigationBar, 0x2838dbb80, {{0.0, 20.0}, {375.0, 44.0}}, identifier: 'TableView'
                    Button, 0x2838dbc60, {{0.0, 20.0}, {62.0, 44.0}}, label: '首页'
                    StaticText, 0x2838dbd40, {{147.0, 32.0}, {81.0, 20.5}}, label: 'TableView'
                  Other, 0x2838dbe20, {{0.0, 0.0}, {375.0, 667.0}}
                    Other, 0x2838dbf00, {{0.0, 0.0}, {375.0, 667.0}}
                      Other, 0x2838de760, {{0.0, 64.0}, {375.0, 603.0}}
                        Table, 0x2838debc0, {{0.0, 64.0}, {375.0, 611.0}}
                          Cell, 0x2838dea00, {{0.0, 64.0}, {375.0, 43.5}}
                            Image, 0x2838d0000, {{15.0, 73.5}, {24.0, 24.0}}
                            StaticText, 0x2838d00e0, {{54.0, 64.0}, {306.0, 43.5}}, label: '测试: 0'
                            Other, 0x2838d01c0, {{54.0, 107.0}, {321.0, 0.5}}
                            Button, 0x2838d02a0, {{281.0, 69.0}, {74.0, 34.0}}, label: '我是按钮'
                              StaticText, 0x2838d0380, {{281.0, 75.0}, {74.0, 22.0}}, label: '我是按钮'
                          Cell, 0x2838d0460, {{0.0, 107.5}, {375.0, 43.5}}
                            Image, 0x2838d0540, {{15.0, 117.0}, {24.0, 24.0}}
                            StaticText, 0x2838d0620, {{54.0, 107.5}, {306.0, 43.5}}, label: '测试: 1'
                            Other, 0x2838d0700, {{54.0, 150.5}, {321.0, 0.5}}
                            Button, 0x2838d07e0, {{281.0, 112.5}, {74.0, 34.0}}, label: '我是按钮'
                              StaticText, 0x2838d08c0, {{281.0, 118.5}, {74.0, 22.0}}, label: '我是按钮'
                          ...后面 cell 就省略了, 都是一样的数据格式
            TabBar, 0x2838f0fc0, {{0.0, 618.0}, {375.0, 49.0}}
              Button, 0x2838f10a0, {{2.0, 619.0}, {184.0, 48.0}}, label: '首页', Selected
              Button, 0x2838f1180, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的'

其实逻辑挺简单, 就是找出 cell, 然后并且点击而已
其实最关键的字段是 ==isHittable==, 请看以下代码, 虽然有点长. 不过请耐心看完



class XQUITestDemoUITests: XCTestCase {

  /// 测试 tableView
  func testTableView() {

    // 初始化, 并打开APP
    let app = XCUIApplication()
    app.launch()

    // 读取 tableView 元素
    let tables = app.tables.firstMatch

    // 获取下标 30 的 cell
    let cell = tables.cells.element(boundBy: 30)
    // 调用封装的方法, 滚动到该 cell
    if tables.xq_scrollToElement(element: cell) {
      // 已经找到 cell, 点击 cell
      cell.tap()
    }

  }

}


/// 对于 tableview 的封装
extension XCUIElement {
    /// 滚动到某个元素
    /// 默认向下滚动
    /// 这里可以再封装一下的,比如可以向上滚动, 可以无限循环上下滚动等等...
    /// - Parameter element: UI元素
    /// - Parameter isAutoStop: true 滚动到最后一个, 自动停下来
    ///
    /// 返回 true 表示找到了传入的元素
    ///
    func xq_scrollToElement(element: XCUIElement, isAutoStop: Bool = true) -> Bool {

        // 判断是否是, tableView
        if self.elementType != .table {
            return false
        }

        // 一直滚动到某个元素可被点击为止
        while !element.isHittable {
            
            // 滚动到最后就停下来
            if isAutoStop {
              // 获取最后一个元素
                let lastElement = self.cells.element(boundBy: self.cells.count - 1)
                // 滚动到最后了, 那么就停下来
                if lastElement.isHittable {
                    return false
                }
            }
            
            self.swipeUp()
        }

        return true
    }
}

直接打开其他 App

这里举例打开 Safari.

// 传入 bundle id, 初始化某个 app
let safariApp = XCUIApplication.init(bundleIdentifier: "com.apple.mobilesafari")
safariApp.launch()

当然, 我们打开 Safari 之后, 也能取到 safari 上面的元素, 并且能操作

系统桌面 App(springboard)

如果我们想去获取当前状态栏上面的信息. 比如电量, 是否正常充电, 信号强度这些的
其实可以通过初始化桌面APP,来获取的

// 注意, 这里不用 launch() 了 
let springboard = XCUIApplication.init(bundleIdentifier: "com.apple.springboard")
// 第一次获取桌面元素信息, 有时候会特别慢...所以这里并不是卡死了, 请耐心等待
// 不知道其中缘由,感觉有点玄学
// 反正我测的时候一般要等待 3 ~ 20 秒
print(safariApp.debugDescription)

当然,我们可以调用 Home 键, 回到桌面, 然后根据获取的信息, 去点击桌面APP,这样也可以行得通
这个 springboard 可以搞很多骚操作, 具体可看我项目, 里面有一些实际用例

申请系统权限, 点击系统权限弹框(例如通知权限)

代码如下


class XQUITestDemoUITests: XCTestCase {

    /// 测试系统按钮自动点击, 通知权限
    func testSystemAlertNotification() {

      // 初始化, 并打开APP
      let app = XCUIApplication()
      app.launch()
        
        // 点击 app 里面的 cell, 去申请通知权限
      let view = app.windows.cells.element(boundBy: 9)
      view.tap()
        
        // 调用封装好的方法, 点击下标 1 的系统 Alert 按钮
        // 下标 1, 就是右边同意按钮
      self.xq_tapSystemAlert(index: 1)
        
        // 等待一会
      let _ = app.wait(for: .notRunning, timeout: 3)
    }

}
    


extension XCTestCase {

    /// 点击系统弹框
    /// - Parameter index: 按钮的下标.
    /// 下标是从左边开始算起, 0为起始下标.   就比如通知权限, 要同意的话, 就传入 1
    func xq_tapSystemAlert(index: Int) {
        let springboard = XCUIApplication.init(bundleIdentifier: "com.apple.springboard")
        springboard.xq_tapAlert(index: index)
    }
    
}

extension XCUIApplication {
    
    ///
    /// 注意, actionSheet 的弹框是没办法调用这个点击的.
    /// 因为 actionSheet 是用两个 ScrollView 组成...并且系统不认为他是一个 alert...
    ///
    
    /// 点击弹框
    /// - Parameter index: 按钮的下标.
    /// 下标是从左边开始算起, 0为起始下标.
    func xq_tapAlert(index: Int) {
        let alerts = self.windows.alerts
        if alerts.count > 0 {
            let _ = self.wait(for: .notRunning, timeout: 1)
            alerts.buttons.element(boundBy: index).tap()
            let _ = self.wait(for: .notRunning, timeout: 1)
        }
    }
    
}

Home键

当前没有发现能双击 Home 键的方法, 有知道的老哥, 请留言告诉我

// 单击 Home 键
XCUIDevice.shared.press(.home)

系统音量按键

// 调节音量, +
XCUIDevice.shared.press(.volumeUp)

// 调节音量, -
XCUIDevice.shared.press(.volumeDown)

Siri

突然唤醒 Siri, 会说话很大声, 在公司玩耍的话, 建议先调小声 😁

// 唤醒 Siri, 并输入语句
XCUIDevice.shared.siriService.activate(voiceRecognitionText: "我帅么?");

设备转方向

注意, 手机要先允许转向才行

// 调节方向
XCUIDevice.shared.orientation = .landscapeLeft

项目地址

不知不觉, 写了那么多...有点啰嗦了😑
这里给上 项目 地址, 想看代码的, 就去下载吧

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