单元测试简介
单元测试是指对项目工程中每一个小的模块来单独进行测试,这些模块可以指一个方法、一个类或者一系列的类组成的一个功能模块。
单元测试的目的就是验证这些模块是否按照预想的逻辑去执行。只有每个模块都能正常运作,最后的应用程序运行时才不会出错。
编写单元测试能够提前发现模块中存在的问题并及时解决,如果不进行单元测试,而是在App所有代码完成时直接看运行效果,这个时候可能会有多个模块同时存在各种各样的问题,同时来解决这些问题肯定会有非常大的难度,所以应尽量避免出现这种情况。下面我们就来介绍怎么使用 XCTest框架来进行单元测试
使用 XCTest 框架进行单元测试
在最新的 Xcode 7 中,Apple 为我们提供了XCTest 框架来进行测试,它不仅可以用来进行单元测试,还可以配合模拟器进行 UI 相关的测试。这里我们主要介绍单元测试的用法,UI 测试就不涉及了。
为工程添加测试支持
如果是新的工程,在创建的时候选中Inclue Unit Tests,Xcode 会自动为我们创建测试需要的样板代码;
如果是已有工程的话, 可以在Project info界面按图中的方式给工程添加测试支持:
然后工程中会多出一个ProjectNameTests的group,并且有一个已经创建好的XCTest的类,下面是这个类的结构:
import XCTest
class UnitTestDemoTests: 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.measureBlock {
// Put the code you want to measure the time of here.
}
}
}
这是一个继承自XCTestCase 的类,一般单元测试所要用的功能都在这个类里,下面介绍一下这几个方法的含义:
- testExample - 这是一个自动生成的测试用例方法,每一个测试用例都需要以test开头,后面可以跟上测试的具体内容,在每个test方法所在的行号旁边,都有一个菱形的图标,点击它就可以运行相应的测试用例。同样,在当前类上也有一个这样的图标,点击的话会运行该类里所有的测试用例。
setUp - 这是一个用来进行初始化的方法,运行每一个测试用例之前,都会先运行这个方法,所以一般会把一些重复的代码放到这里来初始化。
tearDown - 这个方法是每个测试用例运行完成后执行的,可以用来进行释放资源等操作
testPerformanceExample - 也是一个测试用例,它内部通过一个 self.measureBlock 闭包来测试代码的性能。把要测试的代码写到这个闭包里,就可以得出这段代码的运行时间。
使用XCTAssert***编写测试用例
上面介绍了单元测试的基本配置,接下来我们来看一下怎么编写测试用例来测试我们的代码。
XCTest 框架为我们定义了以 XCTAssert 开头的断言,可以很方便的来测试代码的运行结果,下面简单介绍几个
- XCTAssertTrue(expression: BooleanType) - 这个方法用来判断expression是否为true
- XCTAssertNil(expression: Any?) - 这个方法用来判断expression 是否为nil
- XCTAssertEqual(expression1: T?, expression2: T?) - 用来判断 两个表达式是否相等,这个方法还有很多不同泛型的重载,可以用来接收不同的参数类型,如集合类型,字典类型等。
- XCTAssertGreaterThan(expression1: T, expression2: T) - 用来判断expression1是否大于expression2
上面列举的只是一小部分,类似的断言方法还有很多,就不一一列举了。另外对于这些方法,都会有一个带message: String 参数的重载,这个参数可以用来在assert fail 时输出错误信息,以快速定位出错的地方。
下面就通过一个例子来介绍怎么使用这些断言。
首先来看一下要测试的代码内容:
// PeopleGenerator.swift
class PeopleGenerator: NSObject {
func people(withName name: String, age: Int) -> People? {
return People(name: name, age: age)
}
}
// People.swift
class People: NSObject {
var name: String
var age: Int
init?(name: String, age: Int) {
if age < 0 {
return nil
}
self.name = name
self.age = age
super.init()
}
}
在 PeopleGenerator里声明了一个方法people(withName: age:) 这个方法来创建一个 People的实例,通过People类里的构造器可以看出去,在age < 0 的时候可失败构造器会返回nil,这刚好也符合通常的逻辑。
下面我们就针对people(withName: age:)
这个方法来编写测试用例,测试代码是否会按照我们预定的逻辑去执行
import XCTest
@testable import UnitTestDemo // 首先需要import 当前的项目 UnitTestDemo 是当前的工程名,并用 @testable 标记
class UnitTestDemoTests: XCTestCase {
var generator: PeopleGenerator!
override func setUp() {
super.setUp()
generator = PeopleGenerator() // setUp里实例化 generator 对象
}
override func tearDown() {
super.tearDown()
generator = nil // tearDown 里释放资源
}
// 测试用例
func testPeopleGenerate() {
let age = -1
let people = generator.people(withName: "name", age: age)
XCTAssertNil(people, "people is not nil")
}
func testPerformanceExample() {
self.measureBlock {
}
}
}
按照我们代码的逻辑,当 age = -1 的时候,People 类的构造器就会失败,因此我们的测试用例是可以通过测试的:
测试通过后,函数名左边的菱形图标会变成绿色的,如果没有通过,则会变成红色,下面我们再把age 变成 1,再来运行一下测试用例:
不出所料,这次测试是没有通过的,而且还会标识出那一条断言是没有通过的,如果这里写了多条断言,就可以知道是哪一条出错了,同时我们传递的message参数也会出现在错误提示里。这还是很方便的。
测试异步调用
上面讲的这种测试方法只适用于同步调用的情况,当要测试的方法是一个异步的调用,例如一个网络请求,那么用上面的方法就不行了。例如我们用下面的方法来测试网络请求是否正常(注意配置App Transport Security)。
func testAsyncRequest() {
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://www.baid.com")!, completionHandler: { (data, respoonse, error) in
XCTAssertNotNil(data)
XCTAssertNotNil(error)
}).resume()
}
直接这么写的话是也是可以通过的,这是因为还没等到completionHandler 里的XCTAssert运行,testAsyncRequest 方法就直接返回了,所以Xcode 会直接判断当前的测试为通过。但实际情况并不是这样,因为我故意把百度的网址拼错了。
针对这种情况,就需要用到 XCTExpectation 这个类了。我们通过它来给测试的运行加上一定的条件,调用 XCTExpectation 的fulfill 方法来添加条件,只有满足了所有的fulfill 或者timeOut 超时,测试方法才可以返回。下面来看具体用法
func testAsyncRequest() {
let expectation = self.expectationWithDescription("http request")
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://www.baidu.com")!, completionHandler: { (data, respoonse, error) in
XCTAssertNotNil(data)
XCTAssertNotNil(error)
expectation.fulfill() // 满足了fulfill 条件或 timeOut 才可以返回
}).resume()
self.waitForExpectationsWithTimeout(1, handler: nil) // 等待
}
现在把网址改成正确的,再次运行程序,就会发现这次测试会失败,因为我们同时对data 和 error进行了 AssertNotNil,他们中必定有一个不为nil:
但这也说明我们的测试用例写对了,然后去掉 XCAssertNotNil(error) 就可以测试通过了。
到这里XCTest 的单元测试的基本用法就基本讲完了;最后再说一点,有时候Xcode会莫名其妙的不会出现 test 方法前的菱形按钮,这时候可以通过将左边的Navigator 窗口切换到第五个按钮那里,这里显示的就是可以测试的用例,在这里来运行测试也是可以的。
最后来运行一下 class 类里的所有测试用例来作为结束吧: