iOS 9 by Tutorials 笔记(十三)

Chapter 13: Testing

这几年 Apple 在 iOS 测试上改进不少,越来越简单快捷了:

  • Xcode 5 苹果推出了 XCTest 框架的第一个版本,相较上一版 SenTestingKit 增加了很多现代化的实现
  • Xcode 6 增加了异步 asynchronous 和性能 performance 测试
  • 今年随着 Xcode 7 面世 Apple 又推出了 code coverage reports(测试覆盖率报告)和 UI testing

关于测试我之前专门写过两篇文章详述,具体可以看这里: Unit Testing for iOS Part ⅠUnit Testing for iOS Part Ⅱ

本章就挑摘要记录下,不做具体的深入了

Code coverage

开启代码覆盖率可以让你知道整个工程当前的测试情况,开启很简单:在 Product\Scheme\Edit Scheme... 下选择 Test,勾选 Code Coverage — Gather coverage data 就 OK 了

运行测试,在 Xcode 左侧导航栏切换到 report navigator,点击 test action

切换到 Coverage tab,你就能看到测试覆盖率报告了

这个报告展示了基于当前文件的方法测试覆盖情况,你甚至可以进入单独的类中去查看单个方法被测试的次数

@testable imports and access control

通常我们在 test target 中要测试 model 的时候,都要先导入相应的 module,这是因为你的 app 和 test bundle 一般是分离的,但仅仅这样做还不够。

这涉及到了访问控制的概念,通常很多语言都会对从一个代码区块访问另一个代码区块做出限制, Swift 也不例外,在 Swift 中这种访问控制模式基于 modulessource files 的概念。

一个 module 是一个独立的代码分发单元,这可以是一个应用或一个框架,在本例子中,所有在 Workouts app 里的源代码是一个 module,而所有在 testing bundle 中代码是另外一个独立的 module;而一个 source file 是 module 中的一个 Swift 源代码文件,比如 Workout.swift

Swift 提供了三种等级的访问控制:

  1. Public access 本权限下的实例允许任意 module 的代码访问
  2. Internal access 本权限的实例仅允许同一 module 的代码访问
  3. Private access 本权限的实例仅允许在当前 source file 中使用

默认的是 internal,所以想要从 test bundle 访问 app 中的实例是不可能的(因为跨了 module),全部设为 Public 又不现实,苹果审时度势在 Swift 2.0 推出了 @testable,可以让 internal 在 test bundle 中访问到

现在你只需要在 DataModelTests.swift 中将

import Workouts

替换为:

@testable import Workouts

@testable 对 Private access 不起作用

UI testing

如果你的工程是用 Xcode 7 之前版本创建的,那么在写 UI test 之前先要添加 UI testing target

Run your first UI test

第一步将我们要测试的 View 先标记出来

override func viewDidLoad() {  
  super.viewDidLoad()
  tableView.accessibilityIdentifier = "Workouts Table"
}

然后来写我们的 UI 测试方法

func testRaysFullBodyWorkout() {  
  let app = XCUIApplication()
  // 1 得到所有的 table
  let tableQuery = app.descendantsMatchingType(.Table)
  // 2 找出之前标记为 "WorkoutsTable" 的 table
  let workoutTable = tableQuery["Workouts Table"]
  let cellQuery = workoutTable.childrenMatchingType(.Cell)
  let identifier = "Ray's Full Body Workout"
  let workoutQuery = cellQuery
    .containingType(.StaticText, identifier: identifier)
  let workoutCell = workoutQuery.element
  workoutCell.tap()
  // 3 模拟一些点按操作
  let navBarQuery = app.descendantsMatchingType(.NavigationBar)
  let navBar = navBarQuery[identifier]
  let buttonQuery = navBar.descendantsMatchingType(.Button)
  let backButton = buttonQuery["Workouts"]
  backButton.tap()
}

当运行测试的时候,你会发现模拟器会自动启动并模拟整个操作过程。你也许又会问为什么没有 assertions,因为如果界面上某个 UI 元素不存在,测试就会失败。所以执行 UI test 其实暗含了 asserts

UI test classes

在 UI testing 中主要有这三种独立的类

  • XCUIApplication 表示代理当前 App
  • XCUIElement 表示代理当前 UI 元素
  • XCUIElementQuery 用做查询
    • descendantsMatchingType(_:)
    • childrenMatchingType(:_)
    • containingType(_:)

记住 XCUIApplicationXCUIElement 都仅仅是 proxies(代理人),并不是真正的 UI 对象

UI testing convenience methods

现在添加另一个测试,在具体某一项 Workout 详情页面,滚动到底部,点击 Select & Workout 按钮,此时会弹一个警告框,我们点 OK 来 dismiss 掉,最后返回到之前的 list 界面 同样是在 viewDidLoad() 中先标记 detail 页面的 table

tableView.accessibilityIdentifier = "Workout Detail Table"  
func testRaysFullBodyWorkout() {  
  let app = XCUIApplication()
  //1 
  let identifier = "Ray's Full Body Workout"
  let workoutQuery = app.tables.cells
    .containingType(.StaticText, identifier: identifier)
  workoutQuery.element.tap()
  //2
  app.tables["Workout Detail Table"].swipeUp()
  app.tables.buttons["Select & Workout"].tap()
  app.alerts.buttons["OK"].tap()
  //3
  app.buttons["Workouts"].tap()
}
  1. 获取到所有 cells,然后根据 identifier 筛选出来对应的 cell,点击进入详情页面
  2. 向上滚动 table,找到 Select & Workout 按钮点击,弹出的对话框点 OK
  3. 最后点击 navigation bar 上的 Workouts 按钮返回到 list 主页面

运行测试,失败了...

一般有三种方式向下查找到需要的 UI 元素

  1. 如果你有一个唯一的标识可以使用下标,buttonsQuery["OK"]
  2. 使用索引 tables.cells.elementAtIndex(0)
  3. 如果能确保查询到只有一个元素,可以使用 XCUIElementQuery 的 element 属性

如果使用以上三种方法最后找到多个 XCUIElement,那么测试就会失败,这是因为 UI testing framework 不知道你到底想要与哪个 UI 元素进行交互。

而上面的测试失败是因为下面这行找到了两个以 Workouts 命名的按钮

app.buttons["Workouts"]  

注意黄圈圈出来的 Button,修正也很简单,指明我们要点击的按钮是导航栏上的 Workouts 就好

app.navigationBars.buttons["Workouts"].tap()  

UI recording

这个比较简单,新建一个空白的测试方法,光标移到方法内部起始位置,点击红色的 Record UI Test 小圆点会启动模拟器,此时你在模拟器上的操作会被 Xcode 记录下来转换成操作代码,也就是上一步写过的那些代码。


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!