大多数浏览器和
Developer App 均支持流媒体播放。
-
为 Xcode Cloud 创建快速而可靠的测试
了解如何为 Xcode Cloud 制定有效的测试计划,Xcode Cloud 是 Apple 的连续集成和连续交付服务。我们将说明为什么说测试是持续验证您的代码是否正常运行的必备工具。学习如何为 Xcode Cloud 创建快速、可靠而且有效的测试,避免不相关的故障以及快速验证您的代码更改。
资源
相关视频
WWDC23
WWDC22
WWDC21
WWDC20
WWDC19
WWDC18
-
下载
Suzy: 大家好 欢迎来到 “Author fast and reliable tests for Xcode Cloud” 我是 Suzy 从事 XCTest 方面的工作 在本次讲座中 我将分享 开始 Xcode Cloud 测试的 最有效方法 我们的团队将 Xcode Cloud 设计为 适用于所有开发者的强大工具 事实上 我们用它来测试 Xcode 本身 我很喜欢它 我最喜欢 Xcode Cloud 的功能之一 是它能够大大扩展给定的测试套件
通过将大多数测试配置为在云中运行 您现在可以拥有在多个目标上 运行测试的实用方法 包括那些运行不同操作系统版本 以利用不同平台的测试 如 iPhone iPad Apple Watch Apple TV 和 Mac 并运行各种测试计划配置 允许使用运行时分析工具 例如地址和线程检查工具 一旦我们通过了 这样一个全面的测试套件 那么我们就可以确信 代码已经准备好发布了 将测试卸载到 Xcode Cloud 允许运行更广泛的测试集 而不会影响开发者的桌面代码 编译和测试周期 有了这个现在扩展的测试套件 不可靠的测试数量可能会增加 这种情况可能变得无法控制 因此 确保可靠性至关重要 除了可靠性之外 如此大量的测试 还需要高效运行 以限制它们对持续集成过程的影响 让我们先解决可靠性问题 我将演示如何使用 Food Truck 为 Xcode Cloud 编写 更可靠的测试 Food Truck 是一款可以通过 点击和滑动获得美味甜甜圈的 App 通过在 Xcode Cloud 中 运行测试套件 我们能够验证所有 Apple 平台都支持 订购我最喜欢的甜甜圈 巧克力糖屑 Xcode Cloud Workflow 的 每一项改进 都将被识别和演示 有关开始使用 Xcode Cloud Workflows 的更多信息 请观看“Meet Xcode Cloud”
编写更可靠测试的第一步 是确保每个测试的设置和拆卸 都是彻底的 在 Xcode Cloud 中运行的测试 使用了一个 可能不符合 开发者最初假设的新模拟器 让我们确定一些有时会在 测试代码中看到的 设备配置假设 某些测试可能依赖于 特定的日期和时间 例如 服务器可能在不同的时区运行 测试应该避免特定于时区 基于区域设置的值 例如数字格式 和语言方向性可能会导致意外结果 通过显式设置模拟器的语言环境 来避免这个问题 另一个有问题的假设 是依赖某些设备权限 例如互联网访问 最好在单元测试中模拟设备权限 并在 UI 测试中使用警报处理程序 最后 一些测试依赖于预加载的数据 例如 一个测试可能期望 有一个空的文档目录 虽然显式配置模拟器 有时是最简单的选择 但增强测试的设置方法通常更稳健 例如 Food Truck 依赖于 一个菜单文件 作为在设置函数中 实例化卡车对象的一部分 我们生成一个包含甜甜圈菜单项的 模拟数据文件 请注意 我们建议在 setup 方法中 建立所有状态准备 而不是依赖于拆卸方法 来准备后续测试 在许多情况下 可以将只读文件签入存储库 然后通过测试访问 但是 当需要构建这些文件时 Xcode Cloud 支持运行 自定义构建脚本 在该脚本中 您可以一次生成文件 以供多个测试访问 有关如何配置脚本的更多详细信息 请观看“Customize your advanced Xcode Cloud workflow”
这样就完成了正确的模拟器设置 现在 让我们讨论一下如何处理 不满足先决条件的测试 XCTSkip 是指示 XCTest Runner 停止执行当前测试 并将其标记为已跳过的错误 这可用于绕过尚待支持的 操作系统版本或设备类型 您还可以通过设置环境变量 来利用 XCTSkip 以跳过特定于登台或生产环境的测试 让我们回顾一下 如何使用环境变量来控制测试流程
环境变量可以为您设备上的 XCTest 测试运行器 App 和运行 xcodebuild 的 测试主机提供参数 在 Xcode Cloud 中 以 TEST_RUNNER_ 为前缀的环境变量 被传递到 XCTest 测试运行器中 此前缀将在变量 可用于您的代码之前删除 例如 测试代码中 名为 BASE_URL 的变量 将作为名为 TEST_RUNNER_BASE_URL 的 环境变量传入 测试计划需要与测试代码相同的格式 也就是说 我们不添加 TEST_RUNNER_ 前缀 测试代码中的任何地方 都可以引用环境变量 例如 当我们在生产环境中时 它们可以与 XCTSkip 一起使用 以跳过实际订购甜甜圈的测试 当然 除非您饿了 请务必记住 在多个位置 例如测试计划 和 Xcode Cloud 用户界面 重新定义环境变量 可能会导致意外结果 在这种特殊情况下 Xcode Cloud 的环境变量 将优先于项目测试计划中指定的内容 现在我们在测试代码中 引用了环境变量 我们可以在 Xcode Cloud 用户界面中设置它的值
为此 请导航到您的 Cloud Reports 然后按住 Control 键 单击 Food Truck
要在工作流程中编辑环境变量 我们将在上下文菜单中 选择 Manage Workflows 我们正在专门编辑集成工作流 因此我们将双击它 现在 在侧边栏中 我们可以选择 Environment 在工作表中间的 Environment Variables 下 我们可以添加变量的名称和值
作为在 Xcode Cloud Workflow 中 设置环境变量的替代方法 我们可以在测试计划(Test Plan)中 设置它 在此示例中 我们还没有测试计划 要启用测试计划 请打开方案编辑器(Scheme Editor) 在侧边栏中选择 Test 然后单击 Convert to Use Test Plans
好的 现在我们有了一个 我称之为 Food Truck 的测试计划 要想添加环境变量 我们就需要单击测试计划 来打开它的编辑器
在顶部附近 我们可以在 Tests 和 Configurations 之间进行选择 让我们选择 Configurations 现在 在 Arguments 部分 我们将通过单击 Environment Variables 添加变量 会出现一个弹出窗口 我们可以 在其中输入变量的名称和值
现在 在生产环境中 我们的测试将被跳过 要了解有关跳过测试的更多信息 请观看 “XCTSkip your tests”
既然我们已经介绍了 使用环境变量来控制 XCTSkip 现在让我们来谈谈期望超时 测试可能会由于意外超时而失败 例如 这可能是服务器速度慢 或用户界面测试过于紧张的结果 解决这两个问题的一个方法是 增加 XCTestExpectation 超时时间 这样交互就有足够的时间完成 在此示例中 我们将 OrderDonut 超时 从 5 秒增加到 10 秒 以便服务器有更多时间响应 通常最好用 async/await 替换 App 和测试代码超时处理 这种方法允许测试暂停 直到 await 调用结束 而且没有任何超时
我们已经解决了依赖于时间的测试 那就让我们来处理测试套件中的 任何一个测试失败吧 例如 我们有一个依赖于 暂存环境中服务的测试 该服务因维护而停机 我们可以使用 XCTExpectFailure 而不是禁用或跳过此测试 使用 XCTExpectFailure 您的测试将正常执行 结果转换如下 测试中的失败现在将报告为预期失败 而其套件中的失败测试将报告为通过 这种方法消除了预期故障产生的噪音
例如 testOrderDonut 失败 我知道提供订购甜甜圈的服务 现在正在维护中 所以我在此处添加了 对 XCTExpectFailure 的调用 要了解有关 XCTExpectFailure 的更多信息 请观看 “Embrace Expected Failures in XCTest”
现在我们已经声明了预期的失败 让我们利用重复测试来证实代码 并诊断不可靠的代码 重复测试是一种工具 它多次运行相同的测试 等待以下情况 第一次失败 第一次成功或统计结果 例如 在我们的桌面上 我们多次重复运行 我们的新代码和测试用例 以在签入代码之前确认初始 App 和测试代码的可靠性 我们能够检测到 testOrderDonut 只有 80% 的成功率 啊哦 知道故障存在 我们现在使用 重复直到失败(repeat-until-failure)模式 在本地诊断错误 这是利用重复测试的另一种方式 对于依赖于不可靠的外部服务的测试 您可能希望利用 失败时重试(retry-on-failure)重复策略 来确认测试可以成功 虽然重试测试是一种强有力的方法 但是最好尽可能模拟外部服务 模拟服务的优势包括 确定的可靠性和速度 要了解如何模拟依赖关系 请观看“Testing Tips & Tricks” 让我们来探索一下如何实现重复测试
要在您的测试计划中启用重复测试 就需要回到测试计划编辑器 并选择 Configurations
然后 在 Test Execution 部分 有一个弹出窗口 可以选择您的重复测试模式
在这种情况下 我们将选择 Retry on Failure 它主要用于解决不可靠的外部服务 现在我们启用了重复测试模式 有关利用重复测试的更多信息 请观看 “Diagnose unreliable code with test repetition” 所以 我们研究了各种 可用于提高测试可靠性的工具 有关编写质量测试的更多信息 请观看“Write tests to fail” 既然我们的测试是可靠的 那就让它们运行得更快吧 有许多配置选项可以更快地获得结果 让我们尽我们所能 来减少运行测试套件的时间
我们用来提高性能的一种技术 是将我们的测试分成多个测试计划 有时两个就足够了 您可以确定一组简化的测试 作为每次打开 或更新拉取请求的一部分进行验证
例如 我们可以运行单元测试 以及单个平台的 用户界面测试的关键子集
所有受支持平台上的全套测试 仍然可以运行 但现在是在后台运行 不会阻塞拉取请求
这种方法允许我们添加测试和新平台 同时保持持续集成过程的及时性
让我们设置一个工作流 来运行一组选定的测试 在这个例子中 我们已经创建了一个 名为 Pull Requests 的 新测试计划 并在测试计划编辑器中打开它 在靠近顶部的地方 我们可以在 Tests 和 Configurations 之间进行选择 让我们选择 Tests 吧
在这里 我们选择了一个测试子集 来验证拉取请求 现在要设置一个工作流来运行 我们的 Pull Requests 测试计划 我们将回到 Xcode Cloud Manage Workflows 就像我们为了跳过测试 而添加环境变量时所做的那样 要创建新的工作流 我们将单击 Manage Workflow 表左下方的 Add 按钮 为简单起见 我们还将工作流 命名为 Pull Requests 并选择一个开始条件 我们希望此工作流 能够防止任何未通过测试的签入 在侧边栏中 在 Start Conditions 的右侧 我们将单击 Add 按钮
将出现一个菜单 显示启动条件选项 在我们的例子中 我们将选择 Pull Request Changes
现在 我们有了一个 拉取请求开始条件 运行测试要求 首先构建 Food Truck App 为此 我们需要添加一个构建操作 同样在侧边栏中 在 Start Conditions 下面 让我们添加一个操作 我们将单击 Actions 旁边的 Add 按钮 然后从上下文菜单中选择 Build
现在我们有了一个构建 App 的操作 我们将添加另一个操作 来运行我们的测试 我们将再次单击添加操作 但这次我们将选择 Test
太好了 我们有一个测试操作了 我们来选择要运行的测试计划吧 在工作表的中间 有一个用于测试的下拉菜单 在这里 我们可以选择我们的 Pull Requests 测试计划
太棒了 现在我们的工作流配置为 根据拉取请求运行我们的测试计划 要想创建第二个按计划运行 完整测试套件的工作流 您可以遵循一组类似的步骤 但是这一次 选择开始条件为 On a Schedule for a Branch 然后设置工作流 以运行您的全套测试计划 我们在 Xcode Cloud 中 配置了两个工作流 并运行了它们相关的测试计划 要了解有关测试计划的更多信息 请查看“Testing in Xcode”
现在我们已经创建了 拉取请求和计划的工作流测试集 我们可以为速度做出的 另一项改进是同时运行测试 默认情况下 Xcode Cloud 会并行测试您的平台 此外 您可以启用 Xcode 在目标和测试对象类级别上 并行运行测试
要在 Xcode 中启用并行测试执行 我们将再次进入 我们的测试计划编辑器并选择 Tests
然后在 Food Truck Tests 测试包的右侧 我们单击 Options 按钮
其中一个选项允许我们 在可能的情况下“Execute in parallel” 如果服务器有足够的可用内核 则可以同时执行 多个目标和测试对象类 所以让我们启用这个选项 来改善我们的测试套件周转时间
现在我们的测试配置为并行运行 请注意 必须将测试设计为 独立运行以利用并行执行 正确的设置和拆卸 对于可靠的测试用例行为至关重要
在我们的测试并行运行后 是时候将注意力转向失控测试了 失控测试是那些没有及时结束的测试 一些示例包括无限循环 或无限期等待出现故障的服务器
我们可以通过在测试计划中设置 一个执行时间余量来暂停这些测试 执行时间余量指定 测试在因超时错误而失败之前 运行的秒数 这可以防止测试套件卡在单个测试上
在这种情况下 第五次测试由于某种原因卡住了 通过设置执行时间余量 这个失控的测试最终被停止 并标记为失败 然后 XCTest Test Runner 继续运行套件中的下一个测试 让我们为测试计划 配置一个执行时间余量
为了设置执行时间余量 我们将进入测试计划编辑器 并选择 Configurations
在 Test Execution 类别下 我们可以启用 Test Timeouts 并指定等待的秒数 请注意 默认值是 600 秒
配置了最大执行时间余量后 单个失控测试将不再中断 我们的测试工作流程 例如 一个通宵测试套件 现在可以按时完成 并提供一整套有用的结果 耶 我们终于停止了那些失控的测试 所以我们可以继续进行下一个改进了 您可能还记得 我们能够利用重复测试 来提高依赖于外部服务测试的可靠性 我们将测试计划配置为失败时重试 并选择了足够的重复值 但是这些重复可能会增加 运行测试套件所需的时间
不必要的重复是一种浪费 您可能希望将测试重复值 优化为一个较低的数值 此外您可以考虑将有问题的测试 从拉取请求工作流中完全删除 让我们来看看如何做到这一点
我们回到测试计划编辑器中的 重复测试配置
之前我们将重复测试模式设置为 Retry on Failure 现在我们可以调整 Maximum Test Repetitions 值 例如 对于一个 依赖于外部服务器的测试 我们可能选择最多允许 10 次尝试 而它的失败率为 5% 大多数时候 我们会在第一次尝试时成功 但是 如果同一个测试 有一个不相关的错误 它每次都会失败 会将 10 次尝试全部用完 也许 3 次尝试就足够了 会是一个更好的选择
虽然我们希望减少重试以提高性能 但请注意 之前我们建议在某些情况下 增加重试以提高可靠性 因此 这个最小选择值必须 足够可靠地 运行这些测试 这就完成了快速获得结果的配置 想要了解关于 如何更快地获得测试结果的更多信息 请查看 “Get your test results faster”
回顾一下 我们介绍了 开始测试 Xcode Cloud 的 最有效方法 我们专注于配置既可靠又快速的测试 以便您可以避免不相关的失败 并快速验证您的代码更改 谢谢 希望您能享受余下的 WWDC 之旅
-
-
3:37 - setUp()
override func setUp() async throws { }
-
3:46 - setUp() example
var truck: Truck! override func setUp() async throws { let directoryURL = FileManager.default.temporaryDirectory let fileName = UUID().uuidString let fileURL = directoryURL.appendingPathComponent(fileName, isDirectory: false) let data = await mockDonutMenuData() try data.write(to: fileURL) truck = Truck(menuURL: fileURL) }
-
5:55 - Environment variable example
var truck: Truck! func testOrderDonut() throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] let expectation = XCTestExpectation(description: "Order donut") truck.order(with: .sprinkles, host: host) { error, donut in XCTAssertTrue(donut.hasSprinkles) expectation.fulfill() } wait(for: [expectation], timeout: 5) }
-
6:00 - XCTSkip example
var truck: Truck! func testOrderDonut() throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") let expectation = XCTestExpectation(description: "Order donut") truck.order(with: .sprinkles, host: host) { error, donut in XCTAssertTrue(donut.hasSprinkles) expectation.fulfill() } wait(for: [expectation], timeout: 5) }
-
8:18 - XCTSkip example
var truck: Truck! func testOrderDonut() throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") let expectation = XCTestExpectation(description: "Order donut") truck.order(with: .sprinkles, host: host) { error, donut in XCTAssertTrue(donut.hasSprinkles) expectation.fulfill() } wait(for: [expectation], timeout: 5) }
-
8:48 - XCTestExpectation example
var truck: Truck! func testOrderDonut() throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") let expectation = XCTestExpectation(description: "Order donut") truck.order(with: .sprinkles, host: host) { error, donut in XCTAssertTrue(donut.hasSprinkles) expectation.fulfill() } wait(for: [expectation], timeout: 5) }
-
8:59 - Increase XCTestExpectation example
var truck: Truck! func testOrderDonut() throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") let expectation = XCTestExpectation(description: "Order donut") truck.order(with: .sprinkles, host: host) { error, donut in XCTAssertTrue(donut.hasSprinkles) expectation.fulfill() } wait(for: [expectation], timeout: 10) }
-
9:16 - Async/await example
var truck: Truck! func testOrderDonut() async throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") let donut = try await truck.orderDonut(with: .sprinkles, host: host) XCTAssertTrue(donut.hasSprinkles) }
-
10:02 - XCTExpectFailure example
var truck: Truck! func testOrderDonut() async throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") let donut = try await truck.orderDonut(with: .sprinkles, host: host) XCTAssertTrue(donut.hasSprinkles) }
-
10:06 - XCTExpectFailure example
var truck: Truck! func testOrderDonut() async throws { let host = ProcessInfo.processInfo.environment["BASE_URL"] try XCTSkipIf(host == "prod.example.com") XCTExpectFailure("<https://dev.myco.com/bug/98> Donut ordering service is down") let donut = try await truck.orderDonut(with: .sprinkles, host: host) XCTAssertTrue(donut.hasSprinkles) }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。