自动化Test使用详细解析(二) —— 单元测试和UI Test
版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.19 星期五 |
前言
自动化Test可以通过编写代码、或者是记录开发者的操作过程并代码化,来实现自动化测试等功能。接下来几篇我们就说一下该技术的使用。感兴趣的可以看下面几篇。
1. 自动化Test使用详细解析(一) —— 基本使用(一)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
本篇主要了解如何将单元测试和UI测试添加到iOS应用程序,以及如何检查代码覆盖率。
写作测试不是很迷人,但是由于测试可以防止让你的闪亮应用程序变成了一个充满bug的垃圾,这是必要的。 如果您正在阅读本教程,您已经知道您应该为您的代码和UI编写测试,但您可能不知道如何去做。
您可能有一个有效的应用程序,但您想测试您为扩展应用程序所做的更改。 也许您已经编写了测试,但不确定它们是否是正确的测试。 或者,您已经开始研究新的应用程序,并希望随时进行测试。
本教程将向您展示:
- 如何使用Xcode的Test导航器测试应用程序的模型和异步方法
- 如何使用
stubs and mocks
与库或系统对象的交互 - 如何测试UI和性能
- 如何使用代码覆盖工具
在此过程中,您将获得测试ninjas
所使用的一些词汇。
Figuring Out What to Test
在编写任何测试之前,了解基础知识非常重要。 你需要测试什么?
如果您的目标是扩展现有应用程序,则应首先为计划更改的任何组件编写测试。
通常,测试应包括:
- 核心功能:模型类和方法及其与控制器的交互
- 最常见的UI工作流程
- 边界条件
- Bug修复
1. Best Practices for Testing
首字母缩略词FIRST
描述了有效单元测试的一套简明标准。 这些标准是:
- Fast - 快速:测试应该快速进行。
- Independent/Isolated - 独立/隔离:测试不应彼此共享状态。
- Repeatable - 可重复:每次运行测试时都应获得相同的结果。 外部数据提供者或并发问题可能导致间歇性故障。
- Self-validating - 自我验证:测试应完全自动化。 输出应该是“通过”或“失败”,而不是依赖于程序员对日志文件的解释。
- Timely - 及时:理想情况下,应在编写测试的生产代码(测试驱动开发)之前编写测试。
遵循FIRST
原则将使您的测试保持清晰且有用,而不是为您的应用程序设置障碍。
打开已经下载的工程文件:
-
BullsEye
基于iOS Apprentice
中的示例应用程序。 游戏逻辑位于BullsEyeGame
类中,您将在本教程中测试它。 -
HalfTunes
是URLSession
教程中示例应用程序的更新版本。 用户可以在iTunes API
中查询歌曲,然后下载和播放歌曲片段。
Unit Testing in Xcode
Test navigator
提供了最简单的测试方法,您将使用它来创建测试目标并针对您的应用程序运行测试。
1. Creating a Unit Test Target
打开BullsEye
项目并按Command-6
打开Test navigator
。
单击左下角的+按钮,然后从菜单中选择New Unit Test Target ...
:
接受默认名称BullsEyeTests
。 当test bundle
出现在Test navigator
中时,单击以在编辑器中打开该包。 如果bundle
未自动显示,请单击其他导航器之一进行故障排除,然后返回Test navigator
。
默认模板导入测试框架XCTest
,并使用setUp()
,tearDown()
和示例测试方法定义XCTestCase
的BullsEyeTests
子类。
运行测试有三种方法:
- 1)
Product ▸ Test or Command-U
。 这两个都运行所有测试类。 - 2) 单击
Test navigator
中的箭头按钮。 - 3) 单击
gutter
中的菱形按钮。
您还可以通过在Test navigator
或gutter
中单击其菱形来运行单个测试方法。
尝试不同的方式来运行测试,以了解它需要多长时间以及它看起来像什么。 样本测试还没有做任何事情,所以它们运行得非常快!
当所有测试成功后,菱形将变为绿色并显示复选标记。 您可以单击testPerformanceExample()
末尾的灰色菱形以打开性能结果:
本教程不需要testPerformanceExample()
或testExample()
,因此请删除它们。
2. Using XCTAssert to Test Models
首先,您将使用XCTAssert
函数来测试BullsEye
模型的核心功能:BullsEyeGame
对象是否正确计算了一轮的分数?
在BullsEyeTests.swift
中,在import
语句下方添加以下行:
@testable import BullsEye
这使得单元测试可以访问BullsEye
中的internal
类型和功能。
在BullsEyeTests
类的顶部,添加以下属性:
var sut: BullsEyeGame!
这为BullsEyeGame
创建了一个占位符,它是System Under Test (SUT)
,或者是测试用例类与测试有关的对象。
接下来,用这个替换setup()
的内容:
super.setUp()
sut = BullsEyeGame()
sut.startNewGame()
这会在类级别创建BullsEyeGame
对象,因此此测试类中的所有测试都可以访问SUT
对象的属性和方法。
在这里,您还可以调用游戏的startNewGame()
来初始化targetValue
。 许多测试将使用targetValue
来测试游戏是否正确计算得分。
在您忘记之前,请在tearDown()
中释放您的SUT
对象。 将其内容替换为:
sut = nil
super.tearDown()
注意:最好在
setUp()
中创建SUT
并在tearDown()
中释放它,以确保每个测试都以一个干净的平板开始。 有关更多讨论,请查看Jon Reid’s post关于此主题的帖子。
Writing Your First Test
现在,您已经准备好编写第一个测试了!
将以下代码添加到BullsEyeTests
的末尾:
func testScoreIsComputed() {
// 1. given
let guess = sut.targetValue + 5
// 2. when
sut.check(guess: guess)
// 3. then
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}
测试方法的名称始终以test
开头,然后是对其测试内容的描述。
将测试格式化为given, when and then
的部分是一种很好的做法:
-
Given:在这里,您可以设置所需的任何值。 在此示例中,您将创建一个
guess
值,以便指定它与targetValue
的差异。 -
When:在本节中,您将执行正在测试的代码:调用
check(guess :)
。 -
Then:这是您将断言您期望的结果的部分,如果测试失败则打印一条消息。 在这种情况下,
sut.scoreRound
应该等于95(100 - 5)
。
单击gutter
或Test navigator
中的菱形图标运行测试。 这将构建并运行应用程序,菱形图标将变为绿色复选标记!
注意:要查看
XCTestAssertions
的完整列表,请转到 Apple’s Assertions Listed by Category。
1. Debugging a Test
BullsEyeGame
故意内置了一个错误,你现在就可以练习找到它。 要查看操作中的bug,您将创建一个测试,从given
部分中的targetValue
中减去5
,并使其他所有内容保持不变。
添加以下测试:
func testScoreIsComputedWhenGuessLTTarget() {
// 1. given
let guess = sut.targetValue - 5
// 2. when
sut.check(guess: guess)
// 3. then
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}
guess
和targetValue
之间的差异仍为5
,因此得分仍应为95
。
在“断点”导航器Breakpoint navigator
中,添加Test Failure Breakpoint
。 当测试方法发布失败assertion
时,这将停止测试运行。
运行测试,它应该在测试失败时停在XCTAssertEqual
行。
在调试控制台中检查sut
和guess
:
guess
是targetValue - 5
但是scoreRound
是105
,而不是95
!
要进一步调查,请使用正常的调试过程:在when
语句中设置一个断点,在BullsEyeGame.swift
中设置一个断点,在check(guess :)
中,它会产生difference
。 然后再次运行测试,并跳过let difference
语句以检查应用程序中difference
的值:
问题是difference
是负值,所以得分是100 - ( - 5)
。 要解决此问题,您应该使用difference
的绝对值。 在check(guess :)
中,取消注释正确的行并删除不正确的行。
删除两个断点,然后再次运行测试以确认它现在成功。
2. Using XCTestExpectation to Test Asynchronous Operations
现在您已经学会了如何测试模型和调试测试失败,现在是时候继续测试异步代码了。
打开HalfTunes
项目。 它使用URLSession
来查询iTunes API
并下载歌曲样本。 假设您要修改它以使用AlamoFire
进行网络操作。 要查看是否有任何中断,您应该为网络操作编写测试并在更改代码之前和之后运行它们。
URLSession
方法是异步的:它们立即返回,但直到稍后才完成运行。 要测试异步方法,请使用XCTestExpectation
使测试等待异步操作完成。
异步测试通常很慢,因此您应该将它们与更快的单元测试分开。
创建一个名为HalfTunesSlowTests
的新单元测试目标。 打开HalfTunesSlowTests
类,并在现有import
语句下方导入HalfTunes
应用程序模块:
@testable import HalfTunes
此类中的所有测试都使用默认的URLSession
将请求发送到Apple
的服务器,因此声明一个sut
对象,在setUp()
中创建它并在tearDown()
中释放它。
用以下内容替换HalfTunesSlowTests
类的内容:
var sut: URLSession!
override func setUp() {
super.setUp()
sut = URLSession(configuration: .default)
}
override func tearDown() {
sut = nil
super.tearDown()
}
下面,添加异步测试:
// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
// given
let url =
URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Status code: 200")
// when
let dataTask = sut.dataTask(with: url!) { data, response, error in
// then
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
return
} else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if statusCode == 200 {
// 2
promise.fulfill()
} else {
XCTFail("Status code: \(statusCode)")
}
}
}
dataTask.resume()
// 3
wait(for: [promise], timeout: 5)
}
此测试检查向iTunes
发送有效查询是否返回200
状态代码。 大多数代码与您在应用程序中编写的代码相同,使用以下附加行:
- 1)
expectation(description :)
:返回存储在promise
中的XCTestExpectation
对象。description
参数描述了您期望发生的事情。 - 2)
promise.fulfill()
:在异步方法的完成处理程序的成功条件闭包中调用它来标记已满足期望。 - 3)
wait(for:timeout :)
:保持测试运行,直到满足所有期望,或超时间隔timeout
结束,以先发生者为准。
运行测试。 如果您已连接到互联网,则应用程序在模拟器中加载后,测试应该需要大约一秒钟才能成功。
3. Failing Fast
失败会伤害,但它不需要永远。
要体验失败,只需从URL
中的“itunes”
中删除's'
即可:
let url =
URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
运行测试。 它失败了,但需要完整的超时间隔! 这是因为你假设请求总是成功,那就是你调用promise.fulfill()
的地方。 由于请求失败,仅在超时到期时才完成。
您可以通过更改以下假设来改进此问题并使测试失败:不要等待请求成功,而是等待直到调用异步方法的完成处理程序。 一旦应用程序收到来自服务器的响应(OK或错误),就会发生这种情况,这符合预期。 然后,您的测试可以检查请求是否成功。
要了解其工作原理,请创建一个新测试。
但首先,通过撤消对url
所做的更改来修复上一个测试。
然后,将以下测试添加到您的类:
func testCallToiTunesCompletes() {
// given
let url =
URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
let promise = expectation(description: "Completion handler invoked")
var statusCode: Int?
var responseError: Error?
// when
let dataTask = sut.dataTask(with: url!) { data, response, error in
statusCode = (response as? HTTPURLResponse)?.statusCode
responseError = error
promise.fulfill()
}
dataTask.resume()
wait(for: [promise], timeout: 5)
// then
XCTAssertNil(responseError)
XCTAssertEqual(statusCode, 200)
}
关键的区别在于,简单地输入完成处理程序就能满足期望,而这只需要大约一秒钟的时间。 如果请求失败,则then
断言失败。
运行测试。 它现在应该花费大约一秒钟才能失败。 它失败是因为请求失败,而不是因为测试运行超过了timeout
。
修复url
,然后再次运行测试以确认它现在成功。
后记
本篇主要介绍了单元测试和UI Test使用简单示例,感兴趣的给个赞或者关注~~~