就像上面这张图一样,当我们的项目开始变得复杂以后,有时会在增加功能的同时伴随着 Bug 的产生。
而对于已经上线的产品,我们又希望更新的版本不要将 Bug 带到使用者的面前,所以我们每次 release 前都会尽量地去做 test,
在项目还小的时候,我们可能是通过人为的操作接口来看常用场景是否有问题,而这个测试过程也是重复的。
所以还是想回到通过程式来进行测试,这次就先从 Unit Test开始吧。
初步使用Unit Test
引入测试框架
在刚开始建立Xcode Project的时候,系统会问要不要加入Test相关的内容,如果创建的时候没有加入,可以在“File -> New -> Test -> UI Test/ Unit Test。
建立后会得到一个继承 XCTestCase 的档案。
需要注意的是,在这个类别中定义的测试方法,名称需要以“test开头”,并且“不返回内容”。
被认可的测试方法,左侧会有个方块,用来显示测试是否成功。
做一个简单的测试,可以看到左侧会有测试结果。
Assert – 功能测试(1)
我们这里做一个应用,在 HomeViewController 中,使用者可以通过 Slider 来改变 Key 1 以及 Key 2 的值( Slider min value 为 0 max value 为 10),
当 Key1 为 3 并且 Key2 为 7 的时候,会自动解锁,并且push另外一个画面进来。
我们这里写了一个方法“isUnlockSuccess()”用于判断是否成功解锁:
func isUnlockSuccess(number1: Int, number2: Int) -> Bool { if number1 == 3 && number2 == 7 { return true } else { return false } } |
那么我们来为 isUnlockSuccess 方法写一个测试。
我们的 Project 名称是 Lab-Testing,而UnitTest文件名为Lab_Testing_UnitTest.swift。
我们通过引入 Lab_Testing 项目(我们项目名称本来是 Lab–Testing,但似乎引入的时候只能写 Lab_Testing)来取得HomeViewController并初始化。
@testable import Lab_Testing class Lab_Testing_UnitTest: XCTestCase { var vc: HomeViewController! override func setUp() { super.setUp() // 测试开始前会执行这里 vc = HomeViewController(nibName: "HomeViewController", bundle: nil) } } |
接着我们针对 isUnlockSuccess 来写一段 Test,记得要以 test 开头,接着可以 comment + U 或者点 function 左侧的方块来跑测试,
成功后方块就会变成绿色的勾勾了。
Assert – 功能测试(2)
还是同样的应用,但我们要加入一个功能,从新画面回来的以后,要恢复到初始状态。
于是我们在 HomeViewController 中的 viewDidAppear 中加入 resetSettings 这个方法来初始化内容:
将 key1Value、key2Value 还原成0。
将 slider1、slider2 也回到初始位置(value 为0)
将 Slider 右侧 label 的 text 也显示为 0 (初始化为0)
并且将图片换回“未解锁”的图片。
func resetSettings() { key1Value = 0 key2Value = 0 lockImageView.image = UIImage(named: "icon-lock") slider1?.setValue(Float(key1Value), animated: true) slider2?.setValue(Float(key2Value), animated: true) key1Label?.text = "\(key1Value)" key2Label?.text = "\(key2Value)" unlockSliders() } |
对应的测试应该这样写:
func testResetSettings() { vc.resetSettings() XCTAssert(vc.key1Value == 0, "key1Value is not 0 after resetSettings") XCTAssert(vc.key2Value == 0, "key2Value is not 0 after resetSettings") XCTAssert(vc.key1Label.text == "0", "key1Label is not 0 after resetSettings") XCTAssert(vc.key2Label.text == "0", "key2Label is not 0 after resetSettings") XCTAssert(vc.lockImageView.image == UIImage(named: "icon-lock"), "image is not icon-lock after resetSettings") } |
我们在 Lab_Testing_UnitTest 中有先宣告:
var vc: HomeViewController!
并且在 func setUp() 中有初始化它:
override func setUp() { super.setUp() vc = HomeViewController(nibName: "HomeViewController", bundle: nil) } |
需要特别注意的是,这样的初始化马上进行 test 会直接 crash,因为此时的 label, slider 等等,都因为他们都还没有被初始化,而单元测试也不会触发 loadView() ,所以我们需要主动去触发初始化的动作,但 Apple 却不希望我们直接调用 LoadView 方法(参考1、参考2),所以我们通过调用vc.view的方式处理了:
override func setUp() { super.setUp() // 测试开始前会执行这里 vc = HomeViewController(nibName: "HomeViewController", bundle: nil) _ = vc.view } |
其他的Assertions
●XCTAssertEqual
●XCTFail
●XCTAssertEqual
●XCTAssertNil / XCTAssertNotNil
Measure – 性能测试
我们可以通过Measure Block来进行性能测试,比如对 testIsUnlockSuccess() 中加入性能测试:
加入 measure 以后并跑完测试以后,右下角会显示性能测试的结果,如果点开可以看到:
在这里通过 edit 可以设定一个 baseline 以及允许的偏差值,如果运行结果超时就会跳出错误提醒。
Expectation – 异步测试
一个网络请求的例子:
func testUrlRequest() { let url = URL(string: "https://ios.devdon.com/")! let urlExpectation = expectation(description: "GET \(url)") let session = URLSession.shared let task = session.dataTask(with: url) { data, response, error in XCTAssert(data != nil, "data 不应该是 nil") XCTAssert(error == nil, "data 应当是 nil") if let response = response as? HTTPURLResponse, let responseURL = response.url { XCTAssert(responseURL.absoluteString == url.absoluteString, "URL变了") XCTAssert(response.statusCode == 200, "response code 不是200") } else { XCTFail() } urlExpectation.fulfill() } task.resume() waitForExpectations(timeout: task.originalRequest!.timeoutInterval, handler: { error in if let error = error { print("网络请求时发生错误: \(error.localizedDescription)") } task.cancel() }) } func testUrlRequest() { let url = URL(string: "https://ios.devdon.com/")! let urlExpectation = expectation(description: "GET \(url)") let session = URLSession.shared let task = session.dataTask(with: url) { data, response, error in XCTAssert(data != nil, "data 不应该是 nil") XCTAssert(error == nil, "data 应当是 nil") if let response = response as? HTTPURLResponse, let responseURL = response.url { XCTAssert(responseURL.absoluteString == url.absoluteString, "URL变了") XCTAssert(response.statusCode == 200, "response code 不是200") } else { XCTFail() } urlExpectation.fulfill() } task.resume() waitForExpectations(timeout: task.originalRequest!.timeoutInterval, handler: { error in if let error = error { print("网络请求时发生错误: \(error.localizedDescription)") } task.cancel() }) } |
代码覆蓋率(Code Coverage)
XCode 近几年一直在不断的更新,测试方面也是越来越方便了,现在的XCode可以帮我们生成代码覆蓋率的资料,
我们只需要去 Scheme -> Test -> 勾选Code Coverage。
在跑过一轮测试以后,可以在下图中看到代码覆蓋率结果,可以看到我们其实还有很多地方没有测试到。
相关资料可以参考官方文件。
命令行测试(Command Line Testing)
为了更方便地进行测试,官方提供了通过 command line 来进行测试的方法,这样我们可以通过撰写 script 来实现自动化测试。
一个简单的例子,我们指定了测试的项目、Scheme、
xcodebuild test -project Lab-Testing.xcodeproj -scheme Lab-Testing -destination 'platform=OS X,arch=x86_64'
有关更多命令行测试的内容,也请参考官方网站的资料。
在了解了基本的测试方法后,我们可以搭配 Jenkins 来实现更多的自动化测试方法。