第2章 XCTest基础:测试Bundle的创建与配置、测试类的生命周期、测试方法的命名规范
好,咱们正式开始动手写测试了。
这一章,我会带你走一遍 XCTest 的基础流程。说白了,就是搞清楚三件事:测试文件放哪、测试类怎么跑起来、测试方法怎么取名。
这三件事搞明白了,后面写测试就是水到渠成的事。
2.1 测试Bundle的创建与配置
先说说测试 Bundle。你想想看,你的 App 代码是一个 Target,测试代码是另一个独立的 Target。这个独立的 Target,在 Xcode 里就叫「测试 Bundle」。
2.1.1 创建测试 Target
创建方式很简单。我一般用两种方法:
- 新建项目时勾选:创建项目时,勾上「Include Tests」。Xcode 会自动生成 Unit Tests 和 UI Tests 两个 Target。
- 手动添加:项目已经建好了?没问题。点 File → New → Target → iOS Unit Testing Bundle。起个名字,比如
YourAppTests。
我个人习惯把测试 Target 的名字写成 模块名 + Tests。比如网络层叫 NetworkTests,工具类叫 UtilsTests。这样找起来一目了然。
2.1.2 配置测试 Bundle
创建完之后,有几个配置项需要留意:
| 配置项 | 说明 | 建议值 |
|---|---|---|
| Host Application | 测试是否依赖 App 运行环境 | 单元测试一般不勾,UI 测试必须勾 |
| Bundle Identifier | 测试 Target 的唯一标识 | 通常自动生成,如 com.yourapp.tests |
| Info.plist | 测试 Bundle 的配置文件 | 一般不需要手动修改 |
这里有个坑,我曾经踩过:单元测试如果勾选了 Host Application,测试速度会变慢。因为每次运行都要启动整个 App。所以,纯逻辑测试千万别勾。
UIApplication 或 UserDefaults,那就必须勾选 Host Application。否则会 crash。
2.2 测试类的生命周期
每个测试类都继承自 XCTestCase。它有一套固定的生命周期方法。理解这个,你才能控制好测试的「准备」和「清理」。
2.2.1 四个关键方法
class MyTests: XCTestCase {
// 1. 类级别:所有测试开始前调用一次
override class func setUp() {
super.setUp()
// 这里放一次性的初始化,比如加载数据库
}
// 2. 类级别:所有测试结束后调用一次
override class func tearDown() {
// 这里放一次性的清理
super.tearDown()
}
// 3. 方法级别:每个测试方法开始前调用
override func setUp() {
super.setUp()
// 这里放每个测试都需要的数据,比如创建 mock 对象
}
// 4. 方法级别:每个测试方法结束后调用
override func tearDown() {
// 这里放每个测试的清理工作,比如重置状态
super.tearDown()
}
}
嗯,这里要注意:setUp 和 tearDown 是成对出现的。每个测试方法都会走一遍「setUp → 测试方法 → tearDown」这个流程。
2.2.2 实际场景怎么用?
我举个例子。假设你要测试一个网络请求类:
class NetworkServiceTests: XCTestCase {
var sut: NetworkService!
var mockSession: URLSession!
override func setUp() {
super.setUp()
// 每次测试前,创建一个全新的实例
mockSession = MockURLSession()
sut = NetworkService(session: mockSession)
}
override func tearDown() {
// 每次测试后,清空引用
sut = nil
mockSession = nil
super.tearDown()
}
func testFetchData_WithValidURL_ReturnsData() {
// 这里直接用 setUp 里准备好的 sut
let result = sut.fetchData(from: "https://example.com")
XCTAssertNotNil(result)
}
}
为什么要这么写?因为每个测试方法应该是独立的。如果测试 A 改了某个属性,测试 B 再用就会出问题。所以每次 setUp 都重新创建,每次 tearDown 都清理干净。
2.3 测试方法的命名规范
命名这件事,看起来小,但影响很大。我在项目中遇到过,测试方法名写得乱七八糟,跑失败了都不知道测的是啥。
2.3.1 推荐命名格式
我建议用这个格式:
func test[被测方法]_[场景]_[预期结果]()
举个例子:
testFetchData_WithValidURL_ReturnsData()testFetchData_WithInvalidURL_ThrowsError()testLogin_WithCorrectCredentials_ReturnsSuccess()
你看,名字本身就说明了「测什么、什么条件、什么结果」。跑测试的时候,只看名字就知道问题出在哪。
2.3.2 命名注意事项
| 规则 | 说明 | 示例 |
|---|---|---|
| 必须以 test 开头 | XCTest 只识别 test 开头的方法 | testSomething() |
| 使用驼峰命名 | 单词首字母大写,其余小写 | testFetchData_WithEmptyString() |
| 描述要具体 | 不要用 test1、test2 这种 | testSaveData_WhenDiskFull_ReturnsError() |
| 不要包含逻辑 | 名字只描述,不写实现细节 | 避免 testIfArrayIsNotEmpty() |
test() 这样的方法名。Xcode 不会报错,但测试根本不会执行。因为 XCTest 要求方法名必须以 test 开头,且不能有参数。
2.3.3 一个完整的测试类示例
把上面这些串起来,一个完整的测试类长这样:
import XCTest
@testable import MyApp
class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUp() {
super.setUp()
calculator = Calculator()
}
override func tearDown() {
calculator = nil
super.tearDown()
}
func testAdd_TwoPositiveNumbers_ReturnsSum() {
let result = calculator.add(3, 5)
XCTAssertEqual(result, 8)
}
func testAdd_NegativeAndPositive_ReturnsCorrectResult() {
let result = calculator.add(-2, 5)
XCTAssertEqual(result, 3)
}
func testDivide_ByZero_ThrowsError() {
XCTAssertThrowsError(try calculator.divide(10, 0))
}
}
你看,每个方法都独立、清晰、可读。跑测试的时候,哪个方法失败,一眼就能定位。
2.4 本章小结
这一章的内容其实不多,但都是基本功:
- 测试 Bundle:独立的 Target,注意 Host Application 的勾选
- 生命周期:setUp 和 tearDown 成对使用,保证测试独立
- 命名规范:test[方法]_[场景]_[预期],让名字自己说话
下一章,咱们会深入 XCTest 的断言体系。说白了,就是怎么用代码「验证」你的代码是对的。到时候见。