6f83a6a5e6a2046466dd59b4df6f3336
iOS UI 自动化测试原理以及在 Trip.com 的应用实践

前言

笔者入职 Trip.com 已满一年,回顾这一年的工作历程,约一半的时间都在做 UI 自动化测试相关内容。从而,笔者更深入地研究了 iOS 平台下的自动化测试技术,目前也在负责部门 App 自动化测试平台的搭建和维护。故想借这篇文章一并将所踩过的坑以及学习到的技术,系统且全面地整理出分享给大家。

本文的内容大致如下:

  • iOS/macOS UI 自动化测试框架 XCUITest 原理详解
  • 基于 Web Service 的自动化测试平台架构设计
  • Appium 与 Macaca 介绍与对比
  • Trip.com App UI 自动化测试现状

自动化测试可以分为白盒测试、黑盒测试以及灰盒测试,本文主要围绕 Apple 官方提供的 XCUITest 测试框架,逐步阐明 iOS 操作系统下的 UI 自动化测试原理、架构设计思想以及应用场景。

iOS/macOS 自动化测试基础框架 XCUITest

iOS UI自动化测试核心技术

2015 年,Apple 发布了 UI 自动化测试框架 XCUITest 并集成在 Xcode7 中,而 iOS/macOS UI 自动化测试依赖两个核心技术: XCUITest 和 Accessibility

core-technology

core-technology

XCUITest 是集成在 Xcode 中的测试框架,若想使用 UI 测试功能功能,可以在创建 iOS 项目时勾选 Include Tests 选项,从而使项目具备自动化测试的能力。而 Accessibility 技术,则是 Apple 官方为视障用户提供的一整套 iOS/macOS App 的解决方案。

Xcode 项目创建 UITests Target 并运行测试,其编译产物 Test App 本质上是一个 Deamon 守护进程,该进程有独立的应用程序生命周期,依靠 XCUIApplication 类型进行管理。 UITests 的 Test App 进程在运行时会驱动 Host App(项目的主 Target 产物),并且利用元素审查的相关 API 驱动 Host App 模拟用户行为交互,从而进行 UI 自动化测试。

对于 Accessibility 技术,开发人员需要注意的是,XCUITest 框架默认并不能将所有视图元素审查到,只会审查到可以被 VoiceOver 功能读取文字的元素。比如,UIButton 和 UILabel,这些视图对于视障用户而言可以通过语音来获知其内容,而对于 UIImageView、 UIView 这种对于视障人士并不友好的 UIKit 视图元素默认是不会审查到的,所以编码时要另行配置 Accessibility 相关属性,以保证其支持 Accessibility 从而在 UI 自动化查询的元素层级中可见。

基于 XCUITest 框架 和 Accessibility 技术的自动化测试,有利于 App 进行数据一致性校验,但 UI 一致性校验能力较弱。比如,App 可以针对某些数据请求结果或者某个元素是否存在进行校验,而视觉展示效果却仍需要人工介入。

XCUITest 框架结构

XCUITestAPI

XCUITestAPI

XCUITest 测试框架 API 主要包含:元素查询(UI Element Queries)相关类型,如 XCUIElementQuery,UI 元素(UI Elements)相关类型,如 XCUIElement,以及测试 App 生命周期类型(Application Lifecycle)类型,如 XCUIApplication

接下来,我们创建一个简单 Demo 项目,来学习如何使用 XCUITest 框架编程,并进行 iOS UI 自动化测试。

利用 Xcode UITests Target 进行自动化测试

integrate-tests

integrate-tests

创建一个 Demo 工程,勾选 Include Tests 选项,在 ViewController 里编写如下代码。本文 Demo 工程可访问链接 https://github.com/niyaoyao/UITestDemo

import UIKit

class ViewController: UIViewController {
    lazy var testImageView: UIImageView = {
        let testImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        testImageView.backgroundColor = .red
        testImageView.accessibilityIdentifier = "test imageview"
        return testImageView
    }()
    lazy var testLabel: UILabel = {
        let testLabel = UILabel(frame: CGRect(x: 0, y: 130, width: 100, height: 20))
        testLabel.backgroundColor = .green
        testLabel.text = "test label"
        return testLabel
    }()
    lazy var testView: UIView = {
        let testView = UIView(frame: CGRect(x: 0, y: 170, width: 100, height: 50))
        testView.backgroundColor = .blue
        testView.accessibilityIdentifier = "test view"
        return testView
    }()
    lazy var testButton: UIButton = {
        let testButton = UIButton(frame: CGRect(x: 0, y: 230, width: 100, height: 50))
        testButton.backgroundColor = .yellow
        testButton.setTitle("测试按钮", for: .normal)
        return testButton
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(testImageView)
        view.addSubview(testLabel)
        view.addSubview(testView)
        view.addSubview(testButton)
    }
}

源码解释,上面的这段代码创建了四个视图实例,分别为 UIImageView、UILabel、UIView 和 UIButton 类型,并将四个视图实例添加到当前页面中。其中,UILable 和 UIButton 仅设置了frame、字符串、背景颜色等属性,但是对于 UIImageView 和 UIView 视图除了一般的视图属性,还设置了 accessibilityIdentifier 个属性是为了让 UIImageView 和 UIView 支持 Accessibility 功能,但仅设置这个属性并不能使这两个视图在 Accessibility 的元素层级结构中可见。接下来就对 Accessibility 功能做简要介绍。

让 App 支持辅助功能

使用 Accessibility Inspector

前文中提到 Apple 对于视图元素会默认审查能够通过 VoiceOver 播放文字的视图元素,而对于 UIImageView、UIView 这种默认不支持 Accessibility 功能的需要配置相关特性,而开发人员在开发过程中可以通过 Accessibility Inspector 查看不同进程的 Accessibility 元素层级,该应用可以审查 iOS 和 macOS 的元素。
accessibility-inspector

accessibility-inspector

选择 Xcode 的图标菜单并选择 Open Developer Tool 选项,点击 Accessibility Inspector 即可开始使用。

accessibility-hierarchy

accessibility-hierarchy

当我们没有设置 isAccessibilityElement 属性时,在 Accessibility 元素层级结构中就无法看到 UIImageView 和 UIView 元素,只能看到 “test label” 和“测试按钮”。而当我们将 UIImageView 和 UIView 的 isAccessibilityElement 属性设置为 true 时, UIImageView 和 UIView 元素才能在元素层级中可见。

accessibility-hierarchy

accessibility-hierarchy

Accessibility 相关属性

UIAccessibility: var accessibilityLabel: String? { get set }

accessibilityLabel 属性可以解决绝大部分的 Accessibility 问题,当光标将焦点放在设置该属性的元素师时,它的内容可由 VoiceOver 读取的人类可读的字符串。但如果不是需要被视障用户获知的视图元素,仅用于自动化测试,就可以不用设置该属性。

UIAccessibility: var accessibilityIdentifier: String? { get set }

accessibilityIdentifier 属性不会被 VoiceOver 诵读,而是面向开发人员的字符串,可在不希望用户操作 accessibilityLabel 的情况下使用。

UIAccessibility: var isAccessibilityElement: Bool { get set }

如果 isAccessibilityElement 未设置为 true,那么这个视图将不会在 Accessibility 视图层次结构中可见。

The default value for this property is false unless the element is a standard UIKit control, in which case, the value is true. —— Apple Documentation

另外,根据 Apple 官方中的介绍 UIControl 的子类的 isAccessibilityElement 属性都默认设置为 true。更多关于 Accessibility 属性相关的内容可以参见 UIAccessibilityUIAccessibilityContainer

手动编写测试 case

import XCTest

class UITestDemoUITests: XCTestCase {

    override func setUpWithError() throws {
        // ...
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample() throws {
        // UI tests must launch the application that they test.
        let app = XCUIApplication()
        app.launch()
        let label = app.staticTexts["test label"]
        XCTAssertTrue(label.exists)
        let button = app.buttons["测试按钮"]
        XCTAssertTrue(button.exists)
        let imgview = app.images["test imageview"]
        XCTAssertTrue(imgview.exists)
        let view = app.otherElements["test view"]
        XCTAssertTrue(view.exists)  
        // Use recording to get started writing UI tests.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    func testLaunchPerformance() throws {
        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
            // This measures how long it takes to launch your application.
            measure(metrics: [XCTApplicationLaunchMetric()]) {
                XCUIApplication().launch()
            }
        }
    }
}

源码解释,XCUIApplication 类型的实例,是管理 Test App 生命周期的实例对象,可以通过该对象获取 Accessibility 视图层级结构,通过 XCTAssertTrue 断言元素是否存在。

录制交互行为自动生成测试 case

对于相对复杂的 Test Case,可以通过 Xcode 提供的测试行为录制功能进行自动代码生成。
execute-test

execute-test

execute-test
execute-test

UITest 执行过程

execute-test

execute-test

点击 Test 定义的 function 前方对应的播放按钮或者 Test Navigator 中对应 function 的播放按钮,就可以开始执行 UI 测试。而开始 UI 测试后,会先执行源码编译,将 Target 中的源码编译出产物,启动 Test App 进程,进入 Test 程序执行 app.launch() 则会启动 App,然后执行断言源码。

iOS 自动化测试工具链

编写了基本的 UI 测试的 UITest Target 方法之后,我们可以利用相关命令行工具链,将 iOS UI 自动化测试脚本化,从而可以方便集成入 CI 流程。

xcodebuild

xcodebuild test -project UITestDemo.xcodeproj -scheme UITestDemoUITests -destination 'platform=iOS,id=<iPhoneUDID>'

可以利用上述命令执行自动化测试,也可以将命令进行拆分,拆分为测试编译命令和测试执行命令,以便细化自动化测试过程。
测试编译命令:

xcodebuild build-for-testing -project ****.xcodeproj -scheme **** -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,id=XXXXX' -derivedDataPath ~/derived_path -quiet COMPILER_INDEX_STORE_ENABLE=NO GCC_WARN_INHIBIT_ALL_WARNINGS=YES | tee build.log

测试执行命令:

xcodebuild test-without-building -xctestrun ****.xctestrun -configuration Debug -sdk iphonesimulator -destination 'platform=iOS Simulator,id=XXXXXX' -derivedDataPath ~/derived_path -resultBundlePath ****.xcresult -only-testing:****-UITests/TargetTests

xcrun simctl

simctl 命令是 xcrun 的一套自命令,提供一系列用来控制 iOS 模拟器的命令。

列举当前已经启动的模拟器 xcrun simctl list devices | grep booted
启动模拟器 xcrun simctl boot XXXXX
关闭模拟器 xcrun simctl shutdown XXXXX
设置模拟器权限 xcrun simctl privacy XXX grant location-always xx.xx.xxx
安装 App xcrun simctl install {} {}'.format(uuid, app_path)
运行指定 App xcrun simctl launch {} {}'.format(uuid, bundle_id)
结束指定 App xcrun simctl terminate {} {}'.format(uuid, bundle_id)
卸载指定 App xcrun simctl uninstall {} {}'.format(uuid, bundle_id)

ideviceinstaller

与控制模拟器相似,iOS 真机也有相应的控制命令行工具链,例如 ideviceinstaller

安装 apppath 下的 app ideviceinstaller -i apppath
安装 xxx.ipa 为应用在本地的路径ideviceinstaller -u [udid] -i [xxx.ipa]
卸载应用 ideviceinstaller -u [udid] -U [bundleId]
查看设备安装的第三方应用 ideviceinstaller -u [udid] -l
同上,查看设备安装的第三方应用 ideviceinstaller -u [udid] -l -o list_user
查看设备安装的系统应用 ideviceinstaller -u [udid] -l -o list_system
查看设备安装的所有应用 ideviceinstaller -u [udid] -l -o list_all
列出手机上所有的用户安装的app ideviceinstaller -l

ios-deploy

查看当前链接的设备 ios-deploy -c
安装APP ios-deploy --[xxx.app]
卸载应用 ios-deploy --id [udid] --uninstall_only --bundle_id [bundleId]
查看所有应用 ios-deploy --id [udid] --list_bundle_id

top Created with Sketch.