测试基础(XCTest 概念)
本教程共 93 篇 · 第 90 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:理解单元测试在做什么,认识 XCTest 的核心概念(测试类、test 方法、断言),并知道在 Swift 包里测试代码该放哪。
代码写完能跑,不代表它对。你手敲几行 print 看看输出,那是临时验证;而测试是把它固定下来——以后改了代码,一键就能知道老功能有没有被破坏。Swift 长期以 XCTest 为官方测试框架;从 Swift 6.0 起,Apple 又随语言内置了另一套官方框架 Swift Testing(import Testing),用宏来组织测试、语法更贴近 Swift 习惯。本章只讲 XCTest 概念,不动手搭完整项目。
1-1 什么是单元测试
单元测试是把程序拆成最小可验证单元(通常是一个函数、一个类型的行为),单独给它喂输入、检查输出是否符合预期。它的价值不在”证明程序对”,而在”早发现错”。
想象你做一道菜,每加一味调料就尝一口,比整锅炖完才发现咸了要好改得多。单元测试就是那一次次”尝一口”,让 bug 在刚冒头时就被逮住。
Note测试不是越多越好,但关键逻辑、边界情况(空值、零、超大数)一定要有测试守护。初学者先学会给核心功能写测试就够。
1-2 XCTest 框架长啥样
XCTest 是苹果官方框架,已经随工具链自带。它的使用套路非常固定:
- 测试代码写在类里,这个类继承自
XCTestCase。 - 每个想跑的测试方法,名字必须以
test开头(小写 t)。 - 方法里用各种”断言”来声明”这里应该是什么样”。
只要满足这三条,测试运行器就会自动发现并执行它们。你不用手动注册哪个方法要测。
1-3 写一个测试类
下面是一个典型结构。注意它只是演示概念,展示测试长什么样。
import XCTest
@testable import MathTools
class MathToolsTests: XCTestCase {
func testAddition() {
let result = add(2, 3)
XCTAssertEqual(result, 5)
}
func testEmptyInput() {
let list: [Int] = []
XCTAssertTrue(list.isEmpty)
}
}
MathToolsTests 继承 XCTestCase,两个方法都 test 开头,所以都会被识别为测试用例。里面调用的 XCTAssertEqual、XCTAssertTrue 就是断言。
1-4 为什么需要 @testable
默认情况下,Swift 里 internal 级别(不写访问修饰符就是它)的 API 在另一个模块里看不见。测试代码通常放在单独的测试 target,属于另一个模块,于是它访问不到被测代码的内部函数。
@testable import MathTools 解决的就是这个。它让被测模块里 internal 的声明在测试中可见,你不必为了能测而把一切都改成 public。
Tip别为了测试把 API 全改成 public,那会破坏封装。
@testable是官方给的正道:既保住内部性,又能测。
1-5 常用断言方法
断言是测试的灵魂——它说出”期望”。XCTest 提供了一族 XCTAssert* 函数,挑常用的记住:
XCTAssertEqual(a, b):a 和 b 相等。XCTAssertTrue(条件)/XCTAssertFalse(条件):布尔为真 / 为假。XCTAssertNil(x)/XCTAssertNotNil(x):x 为 nil / 非 nil,常用于可选类型。XCTAssertGreaterThan(a, b)等比较类:大小关系。XCTAssertNoThrow(表达式):执行某段代码不应抛错。XCTAssertThrowsError(表达式):执行应当抛错,常用于错误处理的测试。
func testDivisionByZero() {
XCTAssertThrowsError(try divide(10, by: 0))
}
断言一旦不满足,该测试方法立刻标红失败,并打出你给的信息,方便定位。多个断言之间互不影响——前面的失败不会中断后面。
1-6 测试在包里怎么组织
在 SPM 工程里,测试代码放在 Tests/ 目录下,每个被测 target 通常对应一个 XxxTests 文件夹。清单里用 .testTarget 声明,并让它依赖被测 target。
targets: [
.target(name: "MathTools", dependencies: []),
.testTarget(
name: "MathToolsTests",
dependencies: ["MathTools"]
),
]
swift test 命令会编译并运行所有测试 target。在 Xcode 里则是快捷键 Cmd + U 跑全部测试,点侧边钻石图标能单独跑一个。
Warning测试 target 必须依赖被测 target,否则
@testable import找不到模块,编译直接失败。这是新手最常见的配置漏项。
1-7 好测试的几个特征
写测试也有章法,否则测试本身会变成负担。
第一,独立。每个测试方法自己能跑,不依赖别的测试先跑过。别让 testB 必须排在 testA 后面才通过,那样一改就崩一串。
第二,只测一件事。一个方法里聚焦一个行为,失败时你立刻知道哪坏了。塞太多断言反而模糊重点。
第三,覆盖边界。除了正常输入,空数组、负数、nil、超大值这些”边角”最容易藏 bug,值得专门写用例。
1-8 测试失败意味着什么
测试方法里任意断言失败,整个方法标为失败,但其他方法照常跑。运行器最后汇总:通过几个、失败几个、卡在哪个断言。
失败不是丢脸,恰恰是你赚钱的时刻——它拦住了一次会流到用户手里的错误。养成习惯:改完代码先跑测试,全绿再提交。
1-9 setUp 与 tearDown:每个测试的前后准备
测试常需要”先造好环境,再跑,最后清理”。XCTest 提供两个钩子:setUp() 在每个测试方法前调用,tearDown() 在每个测试方法后调用。
override func setUp() {
super.setUp()
// 比如初始化一个临时数据库或示例对象
}
override func tearDown() {
// 比如删除临时文件
super.tearDown()
}
把公共准备放 setUp,能避免每个测试方法重复写一遍,也让测试之间真正独立——因为每个测试都拿到一份全新的初始状态,上次跑的残留不会影响这次。
1-10 异步代码怎么测
现在很多逻辑是异步的:网络请求、文件读写、async/await。XCTest 有对应手段。对于老式回调风格,可用 XCTestExpectation 等期望 fulfill;对于 Swift 并发,测试方法本身能标成 async,直接 await 被测的异步函数,再用普通断言检查结果。
func testFetch() async throws {
let result = try await fetchUser(id: 1)
XCTAssertEqual(result.name, "小明")
}
标了 async 或 throws 的测试方法,XCTest 运行器一样能识别并执行,你不用自己包一层。这让你测并发代码和测同步代码写法几乎一样简单。
1-11 性能测试与断言信息
除了对错,XCTest 还能量时间。measure 块会把里面代码跑很多遍,统计平均耗时,你可以设基线判断”是不是变慢了”。
func testPerformance() {
measure {
_ = (1...1000).map { $0 * 2 }
}
}
另外,断言几乎都支持附带说明字符串,比如 XCTAssertEqual(a, b, "排序后首元素应为最小值")。失败时这条信息会显示出来,帮你秒懂哪错。测试写多了你会明白:断言信息写清楚,等于给半年后的自己留了便条。
1-12 小结
XCTest 的套路就五点:测试类继承 XCTestCase;方法名以 test 开头;用 @testable import 拿到内部 API;靠 XCTAssert* 断言表达期望;测试在 SPM 里放 Tests/ 并用 .testTarget 关联。下一章讲调试——当测试或运行出错时,怎么把问题揪出来。