XCTSkip your tests
Developer Tools 入门 14m

使用 XCTSkip 跳过测试

XCTSkip your tests

2020年6月25日

在 Apple 官方观看视频

一句话判断

终于不用再在测试里写 return 或者 #if canImport 来跳过测试了——XCTSkip 是一个简单但极其实用的 API,用对了能让你的测试报告清晰得多。

这场 Session 讲了什么

Xcode 12 引入了 XCTSkip 系列 API,让开发者可以在测试运行时优雅地跳过特定测试用例。在此之前,跳过测试的方式五花八门:有人用 guard + return 静默跳过,有人用 #if 编译条件排除整个测试,还有人干脆把不需要的测试注释掉。这些做法的共同问题是——测试报告中看不到任何”跳过”的痕迹,你无法区分”这个测试没跑”和”这个测试根本不存在”。

Session 介绍了三种跳过方式:XCTSkip(无条件跳过)、XCTSkipIf(条件为真时跳过)和 XCTSkipUnless(条件为假时跳过)。每个跳过操作都会在测试报告中以黄色标记展示,附带开发者提供的跳过原因说明。这让测试报告变成了一个完整的状态面板——通过、失败、跳过各有多少,一目了然。

典型的使用场景包括:只在特定设备上运行的测试(如相机功能测试在模拟器上跳过)、依赖特定 OS 版本的 API 测试、需要特定硬件的测试(如 LiDAR 传感器)以及需要网络连接的集成测试。

值得深挖的点

XCTSkip 与测试计划的配合

Xcode 12 的测试计划(Test Plan)功能可以配置不同环境下运行不同的测试集。但 XCTSkip 提供了一种更灵活的方案——在代码层面根据运行时条件决定是否跳过。比如你的测试代码可以检测当前设备是否支持 ARKit,如果不支持就 XCTSkip,而不是在测试计划中手动排除。这种方式的优势是测试代码自带了运行条件说明,新团队成员一看就懂。

跳过 vs 失败的语义区别

Session 强调了一个重要的语义区分:跳过是”我知道这个测试在当前环境下不应该运行”,而失败是”这个测试应该通过但没有”。如果你的测试因为缺少前置条件(如服务器未启动)而失败,它应该被标记为跳过而非失败。这样 CI 的红灯只会因为真正的代码问题亮起,不会被环境问题干扰。

代码片段

import XCTest

class CameraTests: XCTestCase {
    
    // 条件跳过:模拟器上没有相机
    func testCameraCapture() throws {
        // 在模拟器上跳过,因为没有真实相机
        try XCTSkipIf(
            TargetEnvironment.isSimulator,
            "相机测试需要真机运行"
        )
        
        // 只在真机上执行的相机测试代码
        let camera = CameraController()
        let photo = try camera.capture()
        XCTAssertNotNil(photo)
    }
    
    // 条件跳过:检查 OS 版本
    func testNewPhotoAPI() throws {
        try XCTSkipIf(
            #available(iOS 14, *) == false,
            "此测试需要 iOS 14 或更高版本"
        )
        
        // iOS 14+ 的新 API 测试
        // ...
    }
}
// 使用 XCTSkipUnless 确保前置条件满足
import XCTest

class NetworkTests: XCTestCase {
    
    func testAPIEndpoint() throws {
        let url = URL(string: "https://api.example.com/health")!
        
        // 检查服务器是否可达,不可达则跳过
        var response: URLResponse?
        let semaphore = DispatchSemaphore(value: 0)
        
        URLSession.shared.dataTask(with: url) { _, resp, _ in
            response = resp
            semaphore.signal()
        }.resume()
        
        semaphore.wait()
        
        try XCTSkipUnless(
            response != nil,
            "测试服务器不可达,跳过网络测试"
        )
        
        // 确认服务器可达后执行实际测试
        let expectation = self.expectation(description: "API 返回数据")
        APIClient().fetchItems { result in
            XCTAssertNotNil(try? result.get())
            expectation.fulfill()
        }
        waitForExpectations(timeout: 5)
    }
}
// 无条件跳过:标记未完成的功能测试
import XCTest

class FeatureTests: XCTestCase {
    
    func testUpcomingFeature() throws {
        // 功能尚未实现,暂时跳过
        throw XCTSkip("功能 X 尚在开发中,暂时跳过此测试")
        
        // 下面的代码不会执行,但在测试报告中会显示为"跳过"
        // 而不是消失在注释中
    }
    
    // 检测特定硬件能力
    func testLiDARScanning() throws {
        try XCTSkipIf(
            !ProcessInfo().isLiDARAvailable,
            "LiDAR 扫描测试需要配备 LiDAR 的设备"
        )
        
        // LiDAR 相关测试...
    }
}

// 辅助扩展
extension ProcessInfo {
    var isLiDARAvailable: Bool {
        // 检查设备是否支持 LiDAR
        guard let device = AVCaptureDevice.default(
            .builtInLiDARDepthCamera, for: .video, position: .back
        ) else { return false }
        return true
    }
}

最佳实践

  • 总是为 XCTSkip 提供清晰的原因描述,让团队成员知道为什么这个测试被跳过
  • 在 CI 中配置测试报告统计跳过数量,如果跳过数突然增加,说明可能有环境问题或功能回退
  • 不要用 XCTSkip 替代修复失败的测试——如果测试本身有 bug,应该修复它而不是跳过
  • 对需要外部依赖的测试(网络、蓝牙、特定硬件),在测试方法开头统一做前置条件检查
  • 把条件跳过逻辑封装成辅助方法(如 requirePhysicalDevice()),提高可读性和复用性

还有什么值得关注

  • Xcode 的测试报告中,跳过的测试会以黄色图标显示,与绿色(通过)和红色(失败)明确区分
  • XCTSkip 可以在 setUp() 方法中使用,一次性跳过整个测试类的所有测试
  • XCTSkip 与 Swift 的 throw 机制完美集成——try + throw XCTSkip() 的写法非常自然
WWDC 2020