1、XCTest框架概述
XCTest是苹果官方的测试框架,是基于OCUnit的传统测试框架,测试编写起来非常简单。
XCTest 的优缺点:
1)优点:与 Xcode 深度集成,有专门的Test 导航栏,
2)缺点:因为受限于官方测试API,因此功能不是很丰富。在书写性和可读性上都不太好。在测试用例太多的时候,由于各个测试方法是割裂的,想在某个很长的测试文件中找到特定的某个测试并搞明白这个测试是在做什么并不是很容易的事情。所有的测试都是由断言完成的,而很多时候断言的意义并不是特别的明确,对于项目交付或者新的开发人员加入时,往往要花上很大成本来进行理解或者转换。另外,每一个测试的描述都被写在断言之后,夹杂在代码之中,难以寻找。使用XCTest测试另外一个问题是难以进行mock或者stub。
2、XCTestCase概述
XCTestCase是苹果官方提供的一个单元测试工具,它的初始化不是用户控制的,开发者无需手动针对XCTestCase的subclass进行alloc和init或者调用静态方法初始化的操作。
针对一个功能模块的单元测试(针对某个class),只需要单独给这个类创建一个继承于XCTestCase,在文件中实现下面基本函数后(一般系统会默认创建这三个函数),需要测试的逻辑只需要开发者自行定义以test开头的函数,然后在那边实现自己针对某个函数、返回数值结果、操作等的测试脚本即可,按comman+u执行,函数头上出现出现蓝色的标记表示通过测试,否则直接报红色错误。
import XCTest
@testable import test
class testTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
从注释我们可以知道这4个函数的意思
函数 | 用途 |
setUp |
继承与XCTestCase 函数测试文件开始执行的时候运行 |
tearDown | 继承与XCTestCase 测试函数运行完之后执行 |
testExample | 测试的例子函数 |
testPerformanceExample | 性能测试 |
3、使用XCTest框架进行单元测试
1)创建一个单元测试Target
单元测试target的创建方式有2种
方式一:在创建新项目时,勾选 Include Unit Tests,项目创建完成就会生成一个单元测试target,target名称默认为 项目名称+Tests
方式二:在已存在的项目中创建,按comman+5 打开xcode的测试导航器,点击左下角的 + 按钮,然后从菜单中选择 New Unit Test Target…
创建完成,如下图所示,
运行这个测试类的方法有三种:
(1)Product\Test 或者 Command-U。这实际上会运行所有测试类。
(2)点击测试导航器中的箭头按钮。
(3)点击中缝上的钻石图标
2)XCTest的测试范围:
(1)基本逻辑测试处理测试
(2)异步加载数据测试
(3)数据mock测试
3)XCTest常用的一些判断工具都是以”XCT“开头的
//断言,最基本的测试,如果expression为true则通过,否则打印后面格式化字符串
XCTAssert(expression, format...)
//Bool测试:
XCTAssertTrue(expression, format...)
XCTAssertFalse(expression, format...)
//相等测试
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...)
//double float 对比数据测试使用
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
//Nil测试,XCTAssert[Not]Nil断言判断给定的表达式值是否为nil
XCTAssertNil(expression, format...)
XCTAssertNotNil(expression, format...)
//失败断言
XCTFail(format...)
4)XCTest的简单使用
例子说明:
函数 | 说明 |
testExample |
全局变量f1 + f2 相加是否等于固定的数,断言是否相等 |
testIsPrimenumber |
判断是否是素数 断言是否返回真 |
import XCTest
@testable import test
class SampleTests: XCTestCase {
var f1 : Float?
var f2 : Float?
override func setUp() {
super.setUp()
//在测试方法执行前设置变量
f1 = 10.0
f2 = 20.0
}
override func tearDown() {
//在测试方法执行完成后,清除变量
super.tearDown()
}
func testExample() {
XCTAssertTrue(f1! + f2! == 30.0)
}
//simpleTest
func testIsPrimenumber(){
let oddNumber = 5
XCTAssertTrue(isPrimenumber(Double(oddNumber)))
}
func isPrimenumber(_ number : Double)->Bool{
for no in 1...Int(sqrt(number)) {
if Int(number)/no != 0{
return true
}
}
return false
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
5)XCTAssert测试模型
(1)创建一个BullsEyeGame模型类
import Foundation
class BullsEyeGame {
var round = 0
let startValue = 50
var targetValue = 50
var scoreRound = 0
var scoreTotal = 0
init() {
startNewGame()
}
func startNewGame() {
round = 0
scoreTotal = 0
startNewRound()
}
func startNewRound() {
round = round + 1
scoreRound = 0
targetValue = 1 + (Int(arc4random()) % 100)
}
func check(guess: Int) -> Int {
let difference = abs(targetValue - guess)
// let difference = guess - targetValue
scoreRound = 100 - difference
scoreTotal = scoreTotal + scoreRound
return difference
}
}
(2)用XCTAssert测试BullsEyeGame模型类中一个核心功能:一个BullsEyeGame对象能够正确计算出一局游戏的得分吗?
主要步骤:
在BullsEyeTests.swift中,import语句下面添加 :@testable import test
在 BullsEyeTests类头部加入一个属性:var gameUnderTest : BullsEyeGame!
在setup()方法中创建一个新的BullsEyeTests对象,位于 super.setup() 方法后
在 tearDown() 方法中释放你的 SUT 对象,在调用 super.tearDown() 方法之前
开始编写测试方法testScoreIsComputed
注:测试方法的名字总是以 test 开头,后面加上一个对测试内容的描述。
Given-When-Then 结构源自 BDD(行为驱动开发),是一个对客户端友好的、更少专业术语的叫法。另外也可以叫做 Arrange-Act-Assert 和 Assemble-Activate-Assert。
将测试方法分成 given、when 和 then 三个部分是一种好的做法:
在 given 节,应该给出要计算的值:在这里,我们给出了一个猜测数,你可以指定它和 targetValue 相差多少。
在 when 节,执行要测试的代码,调用 gameUnderTest.check(_:)方法。
在 then 节,将结果和你期望的值进行断言(这里,gameUnderTest.scoreRound 应该是 100-5),如果测试失败,打印指定的消息。
import XCTest
@testable import test
class BullsEyeTests: XCTestCase {
var gameUnderTest : BullsEyeGame!
override func setUp() {
super.setUp()
gameUnderTest = BullsEyeGame()
gameUnderTest.startNewGame()
}
override func tearDown() {
gameUnderTest = nil
super.tearDown()
}
// XCTAssert to test model
func testScoreIsComputed() {
// 1. given
let guess = gameUnderTest.targetValue + 5
// 2. when
_ = gameUnderTest.check(guess: guess)
// 3. then
XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}
func testPerformanceExample() {
self.measure {
}
}
}
(3)点击中缝上或者测试导航器上的钻石图标。App 会编译并运行,钻石图标会变成绿色的对勾
6)用 XCTestExpectation 测试异步操作
异步测试需要用到的场景:
(1)打开文档
(2)在其他线程工作
(3)和服务器或者扩展进行交流
(4)网络活动
(5)动画
(6)UI测试的一些条件
举例:网络请求异步测试
步骤:
(1)pod导入alamofire,Target是你要测试的tests Target.
(2)新建期望,用alamofire 发起请求。
(3)请求回调里断言是否为空,fullfill期望看是否满足期望
(4)XCWaiter设置期望完成的时间
import XCTest
import Alamofire
@testable import test
class NetworkAsyncTests: XCTestCase {
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testAsynNetworkTest() {
let networkExpection = expectation(description: "networkDownSuccess")
Alamofire.request("http://www.httpbin.org/get?key=Xctest", method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON { (respons) in
XCTAssertNotNil(respons)
networkExpection.fulfill()
}
//设置XCWaiter等待期望时间,只是细节不同。
// waitForExpectations(timeout: 0.00000001)
// wait(for: [networkExpection], timeout: 0.00000001)
//XCTWaiter.Result 枚举类型如下
/*
public enum Result : Int {
case completed
case timedOut
case incorrectOrder
case invertedFulfillment
case interrupted
}
*/
let result = XCTWaiter(delegate: self).wait(for: [networkExpection], timeout: 1)
if result == .timedOut {
print("超时")
}
}
func testPerformanceExample() {
self.measure {
}
}
}
7)模拟对象和交互
大部分 App 会和系统或库对象打交道——你无法控制这些对象——和这些对象交互时测试会变慢和不可重现,这违背了 FIRST 原则的其中两条。但是,你可以通过从存根获取数据或者写入模拟对象写入来模拟这种交互。
步骤:
(1)在 import语句后导入@testable import HalfTunes
(2)定义SUT是vc以及需要准备好预下载的数据
(3)在setup 函数中设置配置sut
(4)编写测试方法
模拟网络请求: DHURLSessionMock.swift 已经定义好了
模拟数据:https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3 下载得到一个一个 1.txt 之类的文件中。打开它,检查它的 JSON 格式,然后重命名为 abbaData.json,然后将它拖到 testSimulationObjectsTests 的文件组中
import XCTest
@testable import test
class testSimulationObjectsTests: XCTestCase {
//声明SUT被测对象
var controllerUnderTest: SearchViewController!
override func setUp() {
super.setUp()
//构建SUT对象
controllerUnderTest = UIStoryboard(name: "Main",
bundle: nil).instantiateViewController(withIdentifier: "SearchVC") as! SearchViewController
//获取下载好并拖入项目的json文件
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)
//项目中有一个 DHURLSessionMock.swift 文件。它定义了一个简单的协议 DHURLSession,包含了用 URL 或者 URLRequest 来创建 data taks 的方法。还有实现了这个协议的 URLSessionMock 类,它的初始化方法允许你用指定的数据、response 和 error 来创建一个伪造的 URLSession
//构造模拟数据和response
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)
//创建一个伪造的session对象
let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
//将伪造的对象注入app的属性中
controllerUnderTest.defaultSession = sessionMock
}
override func tearDown() {
//释放SUT对象
controllerUnderTest = nil
super.tearDown()
}
// 用 DHURLSession 协议和模拟数据伪造 URLSession
func test_UpdateSearchResults_ParsesData() {
// given
let promise = expectation(description: "Status code: 200")
// when
XCTAssertEqual(controllerUnderTest?.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 = controllerUnderTest?.defaultSession.dataTask(with: url!) {
data, response, error in
// 如果 HTTP 请求成功,调用 updateSearchResults(_:) 方法,它会将数据解析成 Tracks 对象
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
promise.fulfill()
self.controllerUnderTest?.updateSearchResults(data)
}
}
}
dataTask?.resume()
waitForExpectations(timeout: 5, handler: nil)
// then
XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}
// Performance
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.controllerUnderTest?.startDownload(track)
}
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
8)模拟写入mock对象
BullsEye有两种游戏方式:用户可以拖动 slider 来猜数字,也可以通过 slider 的位置来猜数字。右下角的 segmented 控件可以切换游戏方式,并将 gameStyle 保存到 UserDefaults。
这个测试将检查 app 是否正确地保存了 gameStyle 到 UserDefaults 里。
步骤:
(1)在 BullsEyeMockTests 中声明 SULT 和 MockUserDefaults 对象
(2)在 setup() 方法中,创建 SUT 和伪造对象,然后将伪造对象注入到 SUT 的属性中
(3)在 tearDown() 中释放 SUT 和伪造对象
(4)模拟和 UserDefaults 的交互
import XCTest
@testable import test
//MockUserDefaults 重写了 set(_:forKey:) 方法,用于增加 gameStyleChanged 的值。通常你可能认为应当使用 Bool 变量,但使用 Int 能带来更多的好处——例如,在你的测试中你可以检查这个方法是否真的被调用过一次。
class MockUserDefaults: UserDefaults {
var gameStyleChanged = 0
override func set(_ value: Int, forKey defaultName: String) {
if defaultName == "gameStyle" {
gameStyleChanged += 1
}
}
}
class BullsEyeMockTests: XCTestCase {
//声明 SULT 和 MockUserDefaults 对象:
var BullsEyeUnderTest: BullsEyeGameViewController!
var mockUserDefaults: MockUserDefaults!
override func setUp() {
super.setUp()
//创建 SUT 和伪造对象,然后将伪造对象注入到 SUT 的属性中
BullsEyeUnderTest = UIStoryboard(name: "Main",
bundle: nil).instantiateViewController(withIdentifier: "BullsEyeGameVC") as! BullsEyeGameViewController
mockUserDefaults = MockUserDefaults(suiteName: "testing")!
BullsEyeUnderTest.defaults = mockUserDefaults
}
override func tearDown() {
//释放 SUT 和伪造对象:
BullsEyeUnderTest = nil
mockUserDefaults = nil
super.tearDown()
}
// 模拟和 UserDefaults 的交互
func testGameStyleCanBeChanged() {
// given
let segmentedControl = UISegmentedControl()
// when
XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
segmentedControl.addTarget(BullsEyeUnderTest,
action: #selector(BullsEyeGameViewController.chooseGameStyle(_:)), for: .valueChanged)
segmentedControl.sendActions(for: .valueChanged)
// then
XCTAssertEqual(mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
4、Code Converage工具使用
Code Converage主要是用来检测测试的覆盖率
1)在product->scheme->Edit Scheme
2)选中test ->option->勾选Code Converage
(3)按command+u 执行测试代码,打开Xcode左边窗口的Report Navigator,找到 Project Log
(4)选择Test可以看到一下界面
(5)再选中coverage。可以查看代码的覆盖率,打开详情,可以点击文件进入查看测试在该文件的覆盖情况,橘黄色的代表还未执行的,绿色代表执行的,右边的次数代表执行的次数
具体代码:github