版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.19 星期五 |
前言
自动化Test可以通过编写代码、或者是记录开发者的操作过程并代码化,来实现自动化测试等功能。接下来几篇我们就说一下该技术的使用。感兴趣的可以看下面几篇。
1. 自动化Test使用详细解析(一) —— 基本使用(一)
2. 自动化Test使用详细解析(二) —— 单元测试和UI Test使用简单示例(一)
Faking Objects and Interactions
异步测试使您确信您的代码会为异步API生成正确的输入。您可能还希望在从URLSession
接收输入时测试您的代码是否正常工作,或者它是否正确更新了用户的默认数据库或iCloud
容器。
大多数应用程序与系统或库对象(您无法控制的对象)进行交互,与这些对象交互的测试可能很慢且不可重复,违反了两个FIRST
原则。相反,您可以通过从stubs
获取输入或通过更新模拟mock
对象来伪造交互。
当代码依赖于系统或库对象时,请使用伪造。您可以通过创建一个假对象来播放该部分并将此伪注入您的代码中来实现此目的。 Jon Reid
的Dependency Injection描述了几种方法。
1. Fake Input From Stub
在此测试中,您将通过检查searchResults.count
是否正确来检查应用程序的updateSearchResults(_ :)
是否正确解析会话下载的数据。 SUT
是视图控制器,您将使用stubs
和一些预先下载的数据伪造会话。
转到Test navigator
并添加新的Unit Test Target
。将其命名为HalfTunesFakeTests
。打开HalfTunesFakeTests.swift
并导入import
语句正下方的HalfTunes
应用程序模块:
@testable import HalfTunes
现在,用以下内容替换HalfTunesFakeTests
类的内容:
var sut: SearchViewController!
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateInitialViewController() as? SearchViewController
}
override func tearDown() {
sut = nil
super.tearDown()
}
这声明了SUT
,它是一个SearchViewController
,在setUp()
中创建它并在tearDown()
中释放它:
注意:
SUT
是视图控制器,因为HalfTunes
有一个巨大的视图控制器问题 - 所有工作都在SearchViewController.swift
中完成。 Moving the networking code into a separate module可以减少此问题,并且还可以使测试更容易。
接下来,您将需要一些示例JSON
数据,您的假会话将为您的测试提供这些数据。 只需要几个项目,所以要限制你的下载结果在iTunes
中附加&limit = 3
到URL
字符串:
https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3
复制此URL
并将其粘贴到浏览器中。 这将下载名为1.txt
,1.txt.js
或类似文件的文件。 预览它以确认它是一个JSON
文件,然后将其重命名为abbaData.json
。
现在,返回Xcode
并转到Project navigator
。 将文件添加到HalfTunesFakeTests
组。
HalfTunes
项目包含支持文件DHURLSessionMock.swift
。 这定义了一个名为DHURLSession
的简单协议,其方法(stubs)
用于创建具有URL
或URLRequest
的数据任务。 它还定义了URLSessionMock
,它符合此协议的初始化程序,允许您使用您选择的数据,响应和错误创建模拟URLSession
对象。
要设置fake
,请转到HalfTunesFakeTests.swift
并在创建SUT
的语句之后在setUp()
中添加以下内容:
let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)
let url =
URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(
url: url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
sut.defaultSession = sessionMock
这将设置虚假数据和响应并创建虚假会话对象。 最后,它将fake
会话注入应用程序作为sut
的属性。
现在,您已准备好编写测试,检查调用updateSearchResults(_ :)
是否解析伪数据。 添加以下测试:
func test_UpdateSearchResults_ParsesData() {
// given
let promise = expectation(description: "Status code: 200")
// when
XCTAssertEqual(
sut.searchResults.count,
0,
"searchResults should be empty before the data task runs")
let url =
URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let dataTask = sut.defaultSession.dataTask(with: url!) {
data, response, error in
// if HTTP request is successful, call updateSearchResults(_:)
// which parses the response data into Tracks
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
self.sut.updateSearchResults(data)
}
promise.fulfill()
}
dataTask.resume()
wait(for: [promise], timeout: 5)
// then
XCTAssertEqual(sut.searchResults.count, 3, "Didn't parse 3 items from fake response")
}
您仍然必须将其写为异步测试,因为stub
是异步方法。
when
断言是在数据任务运行之前searchResults
为空。这应该是真的,因为你在setUp()
中创建了一个全新的SUT
。
fake
数据包含三个Track
对象的JSON
,因此then
断言是视图控制器的searchResults
数组包含三个项目。
运行测试。它应该很快成功,因为没有任何真正的网络连接!
2. Fake Update to Mock Object
上一个测试使用stub
来提供伪对象的输入。接下来,您将使用mock object
来测试您的代码是否正确更新了UserDefaults
。
重新开启BullsEye
项目。该应用程序有两种游戏风格:用户可以移动滑块以匹配目标值,也可以从滑块位置猜测目标值。右下角的分段控件可切换游戏风格并将其保存在user defaults
中。
您的下一个测试将检查应用程序是否正确保存了gameStyle
属性。
在Test navigator
中,单击New Unit Test Class
并将其命名为BullsEyeMockTests
。在import
语句下面添加以下内容:
@testable import BullsEye
class MockUserDefaults: UserDefaults {
var gameStyleChanged = 0
override func set(_ value: Int, forKey defaultName: String) {
if defaultName == "gameStyle" {
gameStyleChanged += 1
}
}
}
MockUserDefaults
覆盖set(_:forKey :)
以增加gameStyleChanged
标志。 通常,您会看到设置Bool
变量的类似测试,但增加Int
会为您提供更大的灵活性 - 例如,您的测试可以检查该方法仅被调用一次。
在BullsEyeMockTests
中声明SUT
和mock object
:
var sut: ViewController!
var mockUserDefaults: MockUserDefaults!
接下来,用这个替换默认的setUp()
和tearDown()
:
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateInitialViewController() as? ViewController
mockUserDefaults = MockUserDefaults(suiteName: "testing")
sut.defaults = mockUserDefaults
}
override func tearDown() {
sut = nil
mockUserDefaults = nil
super.tearDown()
}
这将创建SUT
和mock object
,并将mock object
注入为SUT
的属性。
现在,用以下代码替换模板中的两个默认测试方法:
func testGameStyleCanBeChanged() {
// given
let segmentedControl = UISegmentedControl()
// when
XCTAssertEqual(
mockUserDefaults.gameStyleChanged,
0,
"gameStyleChanged should be 0 before sendActions")
segmentedControl.addTarget(sut,
action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged)
segmentedControl.sendActions(for: .valueChanged)
// then
XCTAssertEqual(
mockUserDefaults.gameStyleChanged,
1,
"gameStyle user default wasn't changed")
}
when
断言是在测试方法更改分段控件之前gameStyleChanged
标志为0
。 因此,如果then
断言也是真的,则意味着set(_:forKey :)
只被调用一次。
运行测试;它应该成功。
UI Testing in Xcode
UI测试允许您测试与用户界面的交互。 UI测试的工作原理是通过查询应用程序的UI对象,合成事件,然后将事件发送到这些对象。 通过API,您可以检查UI对象的属性和状态,以便将它们与预期状态进行比较。
在BullsEye
项目的Test navigator
中,添加一个新的UI Test Target
。 检查要测试的目标是BullsEye
,然后接受默认名称BullsEyeUITests
。
打开BullsEyeUITests.swift
并在BullsEyeUITests
类的顶部添加此属性:
var app: XCUIApplication!
在setUp()
中,使用以下代码替换语句XCUIApplication().launch()
:
app = XCUIApplication()
app.launch()
将testExample()
的名称更改为testGameStyleSwitch()
。
在testGameStyleSwitch()
中打开一个新行,然后单击编辑器窗口底部的红色Record
按钮:
这将在模拟器中以一种模式打开应用程序,该模式将您的交互记录为测试命令。 应用加载后,点按游戏风格开关的滑动Slide
部分和顶部标签。 然后,单击Xcode Record
按钮停止录制。
您现在在testGameStyleSwitch()
中有以下三行:
let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()
Recorder
已创建代码来测试您在应用程序中测试的相同操作。 向slider
和label
发送一个tap
。 您将使用它们作为基础来创建自己的UI测试。
如果您看到任何其他语句,只需删除它们即可。
第一行复制您在setUp()
中创建的属性,因此删除该行。 你不需要点击任何东西,所以也要删除第2行和第3行末尾的.tap()
。现在,打开["Slide"]
旁边的小菜单,选择segmentedControls.buttons [“Slide”]
。
您剩下的应该是以下内容:
app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]
点击任何其他对象,让recorder
帮助您找到您可以在测试中访问的代码。 现在,用这段代码替换这些行来创建给定given
的部分:
// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]
现在您已经在分段控件中有两个按钮的名称,以及两个可能的顶部标签,请在下面添加以下代码:
// then
if slideButton.isSelected {
XCTAssertTrue(slideLabel.exists)
XCTAssertFalse(typeLabel.exists)
typeButton.tap()
XCTAssertTrue(typeLabel.exists)
XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
XCTAssertTrue(typeLabel.exists)
XCTAssertFalse(slideLabel.exists)
slideButton.tap()
XCTAssertTrue(slideLabel.exists)
XCTAssertFalse(typeLabel.exists)
}
当您在分段控件中的每个按钮上tap()
时,将检查是否存在正确的标签。 运行测试 - 所有断言都应该成功。
Performance Testing
从Apple的文档Apple’s documentation中:
A performance test takes a block of code that you want to evaluate and runs it ten times, collecting the average execution time and the standard deviation for the runs. The averaging of these individual measurements form a value for the test run that can then be compared against a baseline to evaluate success or failure.
性能测试需要一段您想要评估的代码并运行十次,收集平均执行时间和运行的标准偏差。 这些单独测量的平均值形成测试运行的值,然后可以与基线进行比较以评估成功或失败。
编写性能测试非常简单:只需将要测量的代码放入measure()
的闭包中即可。
要查看此操作,请重新打开HalfTunes
项目,并在HalfTunesFakeTests.swift
中添加以下测试:
func test_StartDownload_Performance() {
let track = Track(
name: "Waterloo",
artist: "ABBA",
previewUrl:
"http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
measure {
self.sut.startDownload(track)
}
}
运行测试,然后单击measure()
尾随闭包开头旁边的图标以查看统计信息。
单击Set Baseline
以设置参考时间。 然后,再次运行性能测试并查看结果 - 它可能比baseline
更好或更差。 使用Edit
按钮可以将基线重置为此新结果。
每个设备配置存储基线,因此您可以在几个不同的设备上执行相同的测试,并根据特定配置的处理器速度,内存等维护不同的基线。
每当您对可能影响正在测试的方法的性能的应用程序进行更改时,请再次运行性能测试以查看它与基准的比较情况。
Code Coverage
代码覆盖率工具会告诉您测试实际运行的应用程序代码,因此您知道应用程序代码的哪些部分尚未经过测试。
要启用代码覆盖,请编辑scheme’s
的Test
操作,并选中Options
选项卡下的Gather coverage
复选框:
运行所有测试(Command-U)
,然后打开报告导航器(Command-9)
。 选择该列表中顶部项目下的Coverage
:
单击显示三角形以查看SearchViewController.swift
中的函数和闭包列表:
向下滚动到updateSearchResults(_ :)
以查看覆盖率为87.9%
。
单击此功能的箭头按钮以打开源文件到该功能。 当您将鼠标悬停在右侧边栏中的coverage
注释上时,代码部分会突出显示绿色或红色:
覆盖注释显示测试命中每个代码部分的次数;未调用的部分以红色突出显示。 正如您所期望的那样,for
循环运行了3次,但错误路径中没有执行任何操作。
要增加此函数的覆盖范围,您可以复制abbaData.json
,然后对其进行编辑,以便导致不同的错误。 例如,将"results"
更改为"result"
以进行打印测试print("Results key not found in dictionary")
。
1. 100% Coverage?
你有多努力争取100%的代码覆盖率? 谷歌“100%单元测试覆盖率”,你会发现一系列支持和反对的论据,以及对“100%覆盖率”的定义的争论。 反对它的争论说最后10-15%
不值得努力。 支持它的争论说最后10-15%
是最重要的,因为它很难测试。 谷歌“hard to unit test bad design”
,以找到有说服力的论据,即不可测试的代码是更深层次设计问题的标志untestable code is a sign of deeper design problems。
以下是一些可供进一步研究的资源:
- 有关测试主题的几个WWDC视频。 来自WWDC17的两个好的是:Engineering for Testability和Testing Tips & Tricks。
- 下一步是自动化:
Continuous Integration and Continuous Delivery
。 从Apple的Xcode Server
和xcodebuild
的Automating the Test Process,以及维基百科的Wikipedia’s continuous delivery article,该文章借鉴了ThoughtWorks的专业知识。 - 如果你已经有一个应用程序但还没有为它编写测试,你可能想参考Working Effectively with Legacy Code by Michael Feathers,因为没有测试的代码是遗留代码!
-
Jon Reid
的Quality Coding
样本应用档案非常适合学习有关Test Driven Development的更多信息。
后记
本篇主要介绍了单元测试和UI Test使用简单示例,感兴趣的给个赞或者关注~~~