iOS 单元测试详细讲解
单元测试是 iOS 开发中保证代码质量的重要手段,下面我将详细介绍 iOS 单元测试的各个方面。
1. 单元测试基础
什么是单元测试
单元测试是针对软件中最小可测试单元(通常是函数或方法)进行的测试,目的是验证每个单元的行为是否符合预期。
iOS 测试框架
iOS 开发主要使用以下测试框架:
- XCTest: Apple 官方测试框架
- Quick/Nimble: 第三方 BDD 风格测试框架
- OHHTTPStubs: 网络请求模拟框架
2. XCTest 框架详解
测试类结构
swift
swift
import XCTest
@testable import YourAppModule // 使用 @testable 可以访问 internal 级别的成员
class YourTests: XCTestCase {
// 在每个测试方法前调用
override func setUp() {
super.setUp()
// 初始化代码
}
// 在每个测试方法后调用
override func tearDown() {
// 清理代码
super.tearDown()
}
// 测试方法必须以 test 开头
func testExample() {
// 测试代码
}
// 性能测试
func testPerformanceExample() {
self.measure {
// 需要测试性能的代码
}
}
}
常用断言方法
swift
scss
XCTAssertTrue(expression) // 表达式为真
XCTAssertFalse(expression) // 表达式为假
XCTAssertEqual(a, b) // a 等于 b
XCTAssertNotEqual(a, b) // a 不等于 b
XCTAssertNil(expression) // 表达式为 nil
XCTAssertNotNil(expression) // 表达式不为 nil
XCTAssertThrowsError(expression) // 表达式抛出错误
XCTAssertNoThrow(expression) // 表达式不抛出错误
3. 测试实践
测试模型层
swift
scss
func testUserInitialization() {
let user = User(name: "John", age: 30)
XCTAssertNotNil(user)
XCTAssertEqual(user.name, "John")
XCTAssertEqual(user.age, 30)
}
测试网络层
使用 URLProtocol 模拟网络请求:
swift
swift
class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
XCTFail("Handler未设置")
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
func testFetchUser() {
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
let testData = """
{"name": "TestUser", "age": 25}
""".data(using: .utf8)!
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
return (response, testData)
}
let expectation = XCTestExpectation(description: "Fetch user")
let service = UserService(session: session)
service.fetchUser { result in
switch result {
case .success(let user):
XCTAssertEqual(user.name, "TestUser")
XCTAssertEqual(user.age, 25)
case .failure(let error):
XCTFail("请求失败: (error)")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
测试 UI 代码
swift
swift
func testViewControllerTitle() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MyViewController") as! MyViewController
_ = vc.view // 触发 viewDidLoad
XCTAssertEqual(vc.title, "Expected Title")
}
func testButtonAction() {
let vc = MyViewController()
_ = vc.view
vc.myButton.sendActions(for: .touchUpInside)
// 验证按钮点击后的行为
XCTAssertTrue(vc.someProperty)
}
4. 测试驱动开发 (TDD)
TDD 流程:
- 编写一个失败的测试
- 编写最少代码使测试通过
- 重构代码
TDD 示例
swift
swift
// 1. 先写测试
func testStringReversed() {
let input = "hello"
let expected = "olleh"
XCTAssertEqual(input.reversed(), expected)
}
// 2. 实现功能
extension String {
func reversed() -> String {
return String(self.reversed())
}
}
5. 高级测试技巧
测试异步代码
swift
scss
func testAsyncOperation() {
let expectation = XCTestExpectation(description: "Async operation completed")
someAsyncOperation { result in
XCTAssertTrue(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
参数化测试
swift
scss
func testAddition() {
let testCases = [ (2, 3, 5), (0, 0, 0), (-1, 1, 0), (10, -5, 5) ]
for (a, b, expected) in testCases {
XCTAssertEqual(a + b, expected, "(a) + (b) should equal (expected)")
}
}
Mock 和 Stub
swift
less
protocol DataService {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}
class MockDataService: DataService {
var result: Result<Data, Error> = .success(Data())
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
completion(result)
}
}
func testDataProcessing() {
let mockService = MockDataService()
mockService.result = .success("test data".data(using: .utf8)!)
let processor = DataProcessor(service: mockService)
processor.process { result in
XCTAssertTrue(result.isSuccess)
}
}
6. 测试覆盖率
Xcode 提供测试覆盖率报告:
- 编辑 scheme -> Test -> Options
- 勾选 "Gather coverage data"
- 运行测试后可在 Report Navigator 中查看覆盖率
7. 最佳实践
- 测试命名 : 使用
test[被测对象]_[被测条件]_[预期结果]
格式 - 单一职责: 每个测试只验证一个行为
- 独立测试: 测试之间不应有依赖关系
- 快速反馈: 保持测试快速运行
- 避免 UI 测试: 单元测试应专注于业务逻辑
- 测试失败信息: 提供清晰的失败信息
8. 常见问题解决
测试目标找不到模块
确保:
- 测试目标设置了正确的依赖
- 使用
@testable import YourModule
- 模块是可测试的 (Enable Testability 设置为 Yes)
测试不稳定
通常由于:
- 异步操作超时时间不足
- 测试之间有共享状态
- 依赖外部资源 (网络、数据库等)
性能测试波动大
- 在安静的环境下运行
- 关闭其他应用程序
- 多次运行取平均值
通过以上详细的单元测试实践,可以显著提高 iOS 应用的质量和可维护性。 from: deepseek