这几年 Apple 在 iOS 测试上改进不少,越来越简单快捷了:
XCTest
框架的第一个版本,相较上一版 SenTestingKit
增加了很多现代化的实现 关于测试我之前专门写过两篇文章详述,具体可以看这里: Unit Testing for iOS Part Ⅰ , Unit Testing for iOS Part Ⅱ
本章就挑摘要记录下,不做具体的深入了
开启代码覆盖率可以让你知道整个工程当前的测试情况,开启很简单:在 Product/Scheme/Edit Scheme... 下选择 Test ,勾选 Code Coverage — Gather coverage data 就 OK 了
运行测试,在 Xcode 左侧导航栏切换到 report navigator ,点击 test action
切换到 Coverage tab,你就能看到测试覆盖率报告了
这个报告展示了基于当前文件的方法测试覆盖情况,你甚至可以进入单独的类中去查看单个方法被测试的次数
通常我们在 test target 中要测试 model 的时候,都要先导入相应的 module ,这是因为你的 app 和 test bundle 一般是分离的,但仅仅这样做还不够。
这涉及到了访问控制的概念,通常很多语言都会对从一个代码区块访问另一个代码区块做出限制, Swift 也不例外,在 Swift 中这种访问控制模式基于 modules 和 source files 的概念。
一个 module 是一个独立的代码分发单元,这可以是一个应用或一个框架,在本例子中,所有在 Workouts app 里的源代码是一个 module,而所有在 testing bundle 中代码是另外一个独立的 module;而一个 source file 是 module 中的一个 Swift 源代码文件,比如 Workout.swift
Swift 提供了三种等级的访问控制:
默认的是 internal ,所以想要从 test bundle 访问 app 中的实例是不可能的(因为跨了 module),全部设为 Public 又不现实,苹果审时度势在 Swift 2.0 推出了 @testable ,可以让 internal 在 test bundle 中访问到
现在你只需要在 DataModelTests.swift 中将
import Workouts 替换为: @testable import Workouts
@testable 对 Private access 不起作用
如果你的工程是用 Xcode 7 之前版本创建的,那么在写 UI test 之前先要添加 UI testing target
第一步将我们要测试的 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 testing 中主要有这三种独立的类
descendantsMatchingType(_:)
childrenMatchingType(:_)
containingType(_:)
记住 XCUIApplication
和 XCUIElement
都仅仅是 proxies(代理人),并不是真正的 UI 对象
现在添加另一个测试,在具体某一项 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() }
运行测试,失败了...
一般有三种方式向下查找到需要的 UI 元素
buttonsQuery["OK"]
tables.cells.elementAtIndex(0)
如果使用以上三种方法最后找到多个 XCUIElement
,那么测试就会失败,这是因为 UI testing framework 不知道你到底想要与哪个 UI 元素进行交互。
而上面的测试失败是因为下面这行找到了两个以 Workouts
命名的按钮
app.buttons["Workouts"]
注意黄圈圈出来的 Button ,修正也很简单,指明我们要点击的按钮是导航栏上的 Workouts 就好
app.navigationBars.buttons["Workouts"].tap()
这个比较简单,新建一个空白的测试方法,光标移到方法内部起始位置,点击红色的 Record UI Test 小圆点会启动模拟器,此时你在模拟器上的操作会被 Xcode 记录下来转换成操作代码,也就是上一步写过的那些代码。