27. 测试实践
测试的目标是“业务逻辑可验证、回归成本可控”。核心是:可测试的结构 + 稳定的用例。
一、XCTest 基础
import XCTest
@testable import App
final class MathTests: XCTestCase {
func testAdd() {
XCTAssertEqual(1 + 1, 2)
}
}
二、SUT 结构
final class LoginViewModelTests: XCTestCase {
private var sut: LoginViewModel!
private var service: MockLoginService!
override func setUp() {
super.setUp()
service = MockLoginService()
sut = LoginViewModel(service: service)
}
override func tearDown() {
sut = nil
service = nil
super.tearDown()
}
func testLoginSuccess() {
service.result = .success(true)
sut.login(username: "jj", password: "123")
XCTAssertTrue(service.called)
}
}
三、Mock 与协议隔离
protocol LoginService {
func login(username: String, password: String, completion: @escaping (Result<Bool, Error>) -> Void)
}
final class MockLoginService: LoginService {
var result: Result<Bool, Error> = .success(true)
var called = false
func login(username: String, password: String, completion: @escaping (Result<Bool, Error>) -> Void) {
called = true
completion(result)
}
}
四、异步测试
func testAsyncLogin() {
let exp = expectation(description: "login")
service.result = .success(true)
sut.onLogin = { success in
XCTAssertTrue(success)
exp.fulfill()
}
sut.login(username: "jj", password: "123")
waitForExpectations(timeout: 1)
}
五、网络层测试(URLProtocol)
final class MockURLProtocol: URLProtocol {
static var handler: ((URLRequest) -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
guard let handler = MockURLProtocol.handler else { return }
let (response, data) = handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
六、UI 测试
1. 基础示例
final class LoginUITests: XCTestCase {
func testLoginFlow() {
let app = XCUIApplication()
app.launch()
app.textFields["username"].tap()
app.textFields["username"].typeText("jj")
app.secureTextFields["password"].tap()
app.secureTextFields["password"].typeText("123")
app.buttons["login"].tap()
XCTAssertTrue(app.staticTexts["welcome"].exists)
}
}
2. 可测性标识
usernameTextField.accessibilityIdentifier = "username"
passwordTextField.accessibilityIdentifier = "password"
loginButton.accessibilityIdentifier = "login"
七、测试策略
- 业务逻辑优先写单元测试
- UI 测试覆盖关键路径
- Mock 外部依赖,避免真实网络
测试不是把覆盖率做高,而是保证核心流程不会回归。