大多数浏览器和
Developer App 均支持流媒体播放。
-
利用 Swift Testing 进一步优化测试
了解如何使用 Swift Testing 的内置功能编写一系列出色的 (测试) 套件。探索如何进一步优化构建块并借助它们来扩展测试以涵盖更多场景,如何按不同的套件对测试进行分门别类,以及如何优化你的测试以并行运行。
章节
- 0:00 - Introduction
- 0:36 - Why we write tests
- 0:51 - Challenges in testing
- 1:21 - Writing expressive code
- 1:35 - Expectations
- 3:58 - Required expectations
- 4:29 - Tests with known issues
- 5:54 - Custom test descriptions
- 7:23 - Parameterized testing
- 12:47 - Organizing tests
- 12:58 - Test suites
- 13:33 - The tag trait
- 20:38 - Xcode Cloud support
- 21:09 - Testing in parallel
- 21:36 - Parallel testing basics
- 24:26 - Asynchronous conditions
- 26:32 - Wrap up
资源
- Adding tests to your Xcode project
- Forum: Developer Tools & Services
- Improving code assessment by organizing tests into test plans
- Running tests and interpreting results
- Swift Testing
- Swift Testing GitHub repository
- Swift Testing vision document
相关视频
WWDC24
WWDC21
-
下载
大家好 我叫 Jonathan 是 Swift Testing 团队的一员 我和我的同事 Dorothy 将带领大家一起探讨 Swift Testing 中的一些强大功能 它们有助于将测试开发 提升到新的水平 Swift Testing 是一个适用于 Swift 的现代化开源测试库 功能强大且极具表达力 它包含在 Xcode 16 中 如果还未观看“了解 Swift Testing” 请先观看这个视频 了解 Swift Testing 的构建块
测试是开发过程中至关重要的一步 有助于在代码触达用户之前 发现问题 从而让你有信心 发布一个高质量的产品 不过 随着项目中需维护的 测试集合不断增长 你可能会遇到一些挑战 测试会记录并规范代码行为 代码越复杂 测试的可读性和可理解性 就越为重要 需要经过深思熟虑并投入大量工作 才能涵盖代码中 所有可能的边缘情况 组织并关联大量测试 是一项复杂的任务 测试之间隐藏的依赖关系 可能会使测试变得脆弱 容易出现意外故障 下面先由 Dorothy 为我们介绍 测试的可读性非常重要 可读性强的测试更易于处理 也更容易理解测试失败 是由什么导致的 尤其是在代码日益复杂的趋势下 Swift Testing 包含多项功能 有助于你编写 清晰且表达力强的测试 期望值验证是 Swift Testing 中 用来验证代码是否 按预期运行的方法 它们利用 Swift 语言的 特性和语法 来提供极具表达力的简洁接口
这里有一些期望值验证示例 它们使用简单的表达式 来评估 true 或 false 不过 expect 宏的功能更强大 能够处理更为复杂的验证 错误处理通常并没有经过充分测试 但却是用户体验的重要组成部分 你要确保在面对无效输入 和意外情况时 代码能 以一种用户友好的方式处理失败 expect throws 宏在 期望值验证的基础上经过进一步改进 可大大简化这一工作流程 如果你正在测试代码的 正常执行路径 并期望一个可能抛出异常的 函数能够成功返回 只需在测试中调用这个函数即可 如果函数最终抛出错误 测试将会失败 如果函数成功运行并返回一个值 你可以使用期望值验证来 验证它是否符合预期 另一方面 要验证 失败情况是否按预期进行 你需要捕获并检查函数抛出的错误 你可以在代码周围添加自己的 do catch 语句并检查错误 但语句非常冗长 而且如果没有抛出错误 这段代码也不会告诉你 一切顺利进行 Swift Testing 可以帮助 处理 expect throws 宏 你无需自行手动编写 do catch 语句 expect throws 宏 会为你完成这项繁琐工作 如果 brew 函数抛出错误 则测试通过 如果未抛出任何错误 则测试失败
如果要检查是否抛出了 特定类型的错误 可以传递相应类型 而不是“any Error” 如果没有抛出任何错误 则测试失败 如果抛出的错误不是 BrewingError 的实例 测试也同样失败 再进一步 你还可以更加全面地验证 是否抛出了特定错误 对于最复杂的用例 你还可以自定 expect throws 宏所执行的验证 你可以自定错误验证 来检查特定的错误类型或情况 确定关联的值或属性是否正确 以及是否需要执行其他任何操作 以确保代码抛出的 错误适合作业情况 在“了解 Swift Testing”讲座中 Stuart 介绍了 “必要的期望值验证”这一概念 在这里提醒大家 任何常规的期望值 验证 包括会抛出异常的期望值验证 都可以变成必要的期望值验证 在验证可选值时 你可以使用必要的期望值验证 来记录控制流程
检查一个最终为 nil 的值 是没有意义的 因此 添加必要的期望值验证 可以在没有必要进行后续操作时 提前结束测试 接下来 让我们来了解一下 如何使用 withKnownIssue 函数 记录测试中的已知错误 对测试失败进行分类 是一个耗时的过程 如果你无法立即修复失败的测试 或测试失败是由于 你无法控制的因素引起的 这种失败就会给 测试结果增添干扰 从而掩盖真正影响顾客的问题 这项测试是为了检查软冰淇淋机 是否能制作出冰淇淋甜筒 但现在机器似乎出现故障 导致测试失败 机修工可能需要一段时间 才能修好机器 在等机修工上门的过程中 你的第一直觉可能是 在这项测试中使用 disabled 特征 不过 在这种情况下 withKnownIssue 是更好的选择 测试将继续运行 你将收到编译错误的通知 如果函数返回错误 测试结果将不计为测试失败 因为这种失败是预料之中的 相反 这个测试会在结果中 显示为预期失败 如果问题得到修复且不再抛出错误 你会收到通知 然后可以继续操作 并移除 withKnownIssue 调用 进而再次正常运行测试 你的测试也可能会执行多项检查
在这种情况下 你可以仅将失败的 函数封装在 withKnownIssue 中 并让其余验证继续运行 这个部分中我要探讨的 最后一个主题是自定测试描述 在理想世界中 所有测试都能一直通过 冰淇淋机也能一直正常工作 当然 这是不可能的 在现实世界中 测试总会有失败的情况 自定测试描述有助于你 一目了然地了解测试内部的情况 并在出现错误时 引导你找到解决方案 在处理简单枚举时 默认描述通常简明扼要 但是 结构体和类等更复杂的类型 会产生更多的干扰 因为在默认情况下 它们的描述 包含大量额外数据 而这些数据 在测试过程中可能并无用处
这些值会在 Xcode 中 以大量信息的形式显示 这些信息虽然准确 但很难从中找到 可用于区分一个值与另一个值的 重要部分 在这种情况下 你可能希望 在不影响生产代码的前提下 为每个类型提供简洁的测试描述 你可以使自己的类型符合 CustomTestStringConvertible 协议 这样便可以提供量身定制 且特定于测试的描述 现在 你将在 测试导航器和测试报告中 获得可读性和 描述性更强的值 我们已经介绍了如何处理抛出的错误、 如何根据必要的期望值验证来提前 结束测试、如何处理已知的问题以及 如何使测试输出结果更具可读性 测试现已准备就绪 可以处理抛出的任何问题 现在继续有请 Jonathan 正如我们前面提到的 在确保代码质量方面面临的 一项挑战是 涵盖所有边缘情况: 也就是说要包含那些在日常测试中 很少出现的复杂功能 在各种条件下运行测试 以确保捕捉边缘情况 是一种不错的做法 但这在过去需要花费大量时间 而且为每种可能的变化 编写单独的测试 简直是维护工作的噩梦 借助 Swift Testing 你可以 轻松使用多个不同参数 运行单个测试函数 我来演示一下这一点 这个枚举列出了各种冰淇淋口味 你可以检查特定口味是否包含坚果 containsNuts 属性需要 对枚举中的各种情况 提供测试覆盖 Swift Testing 的参数化测试 使得添加测试覆盖范围 变得易如反掌
这个测试函数检查枚举中 是否存在包含坚果的某种情况 在本例中为香草口味 你可以为枚举中的各种情况 编写单独的测试函数 但这需要编写大量代码 在多次拷贝和粘贴同一个函数后 很容易不小心检查错误的值 而你实际上只需要 一两个测试函数即可 毕竟 除了一个输入值之外 测试的逻辑都是相同的 这就是参数化测试 即 一个测试接受一个或多个参数 当测试函数运行时 Swift Testing 会自动将它拆分为 单独的测试用例 每个参数对应一个用例
这些测试用例彼此完全独立 但也能并行运行 这意味着与 for 循环 或单独的测试函数相比 测试所有这些用例所需的时间更短 如果输入类型符合 Codable 则 Xcode 支持重新运行 测试函数的单个测试用例 这样你就可以重试 个别失败的测试用例 而无需重新运行其他 已成功运行的测试用例 让我们详细了解一下如何 对测试函数进行参数化处理 首先 你可以编写一个测试函数 以循环遍历枚举中的所有情况 并逐一进行测试 这个函数是有效的 但我们还可以对它进行改进 一个明显的问题是 如果这个测试函数 对数组中的某个值测试失败 它将停止执行 并且不会测试后续参数 这样可能就不清楚是哪个值失败 你也无法获得想要的覆盖范围 你可以将输入上移 并传递到 Test 属性 而不是在测试函数内 对它们进行迭代 当一个集合传递给 Test 属性 进行参数化处理时 测试库会将集合中的每个元素 作为它的第一个也是唯一的参数 逐个传递给测试函数 如果其中一个参数测试失败 相应的诊断 将清楚地指出哪些输入需要注意 就快完成了 如果你添加第二个函数 来测试那些含有坚果的口味 那么这个枚举的代码覆盖率 将达到 100% 如果将来要扩展枚举 还可以轻松添加新的测试用例
这个示例探讨了枚举的用例 但参数化测试函数也可以 接受许多其他类型的输入 所有可发送的集合 包括数组、字典、范围等 都可以传递给 Test 属性
测试用例及其参数会显示在 测试导航器和测试报告中 前者中的每个参数 都有自己的运行按钮 而后者则会为失败的参数化测试 提供丰富的信息视图 到目前为止 你已经学习了如何编写 包含 1 个输入参数的参数化测试 但如果要传递更多 输入参数该怎么办 Swift Testing 中的测试函数 可以接受多个输入结果 你只需在第一个参数后 添加另一个参数 即可为测试添加额外的参数 第一个集合中的每个元素 都会作为第一个参数 传递给测试函数 第二个集合中的每个元素 则作为第二个参数传递 这两个集合中的所有元素组合 都会自动进行测试 在单个测试函数中 对每种组合进行测试 是提高测试覆盖范围的有效方法 为了帮助直观了解这种方法的 强大之处 让我们来思考两组参数 一组是原材料 另一组是可以用它们制作的食物 一个包含两个参数的测试函数 将测试所有可能的组合 总共有 16 种 你可能会发现自己 正在测试一些奇怪的搭配 我和其他工程师一样 喜欢鸡蛋沙拉和日式蛋包饭 但请告诉我什么是生菜薯条? 这只是每个数组中四个值的情况 随着你向两个集合添加更多的输入 测试用例的数量 将会呈指数级不断增长 为了帮助控制这种指数级增长 测试函数最多只能接受两个集合 你还可以使用 Swift 标准资料库的 zip 函数 来匹配应该组合在一起的输入对 而不是测试原材料 和成品菜肴的每种组合 调用 zip 函数 它们就会进行配对 zip 函数将生成一个元组序列 其中第一个集合中的每个元素 都与第二个集合中的相应元素配对 除此之外不会有其他组合 现在你已经掌握了 利用参数化测试扩大 测试覆盖范围所需的工具 接下来继续由 Dorothy 谈谈如何组织测试 对于这些可帮助你 编写更多测试的新功能 你需要制定策略来加以管理 让我们来了解一下 Swift Testing 为组织测试提供的工具 简而言之 测试套件 是包含测试函数的类型 它们的功能可以用特征来记录 比如 显示名称等 Swift Testing 的新增功能是 测试套件现在可以包含其他套件 从而让你可以更加灵活地组织测试 这是一个相当不错的测试集合 但它的组织并不是非常清晰 这个测试套件包括 热甜点测试和冷甜点测试 通过添加子套件 你可以 在测试本身中反映这种组织结构 并使这些测试组之间的 关系更加明显 标签是另一个有助于 组织测试的特征 一个复杂的软件包或项目 可能包含成百上千个测试和套件 你可能有多个覆盖 代码不同部分的测试套件 虽然它们之间没有直接联系 但某些测试子集 可能具有共同的特征 在这个示例中 有些测试涉及 含咖啡因的食物 有些涉及含巧克力的食物 在这种情况下 标签可帮助你 跨两个套件关联这些测试 请记住 标签不能替代测试套件 套件在源代码级别 对测试函数应用结构体 而标签可帮助你 将不同文件、套件和目标中 具有共同点的测试关联起来 简而言之 这就是标签的作用 但如何 声明标签并将它添加到测试中呢 所有这些饮料均含有咖啡因 任何 值得一品的浓缩咖啡布朗尼也是如此 你可以创建一个 caffeinated 标签 来关联这些测试 即使它们 分布在不同的套件中也无妨 首先 扩展 Tag 类型 并声明一个名为 caffeinated 的 静态变量 变量必须是 Tag 的实例 秘诀是 将标签属性添加到变量 使它可在测试中用作标签
你已创建标签 接下来可以将它添加到这些测试中 你可以在 DrinkTests 的 套件级别进行添加 因为这些测试中使用的 饮料都含有咖啡因 而且测试会从它们的套件中 继承标签 然后 你可以将标签添加到 espressoBrownieTexture 它是 DessertTests 中 唯一含咖啡因的食物 因此不需要将标签添加到 整个 DessertTests 套件中
套件和测试也能包含多个标签 例如 摩卡和浓缩咖啡布朗尼 都含有咖啡因 而且它们都是用巧克力制作的 你可以创建一个 chocolatey 标签 并将它添加到这两个测试中 在测试导航器中 测试按标签分组 这样你就能运行 具有特定标签的测试 我们来看看如何 在 Xcode 16 中使用标签 测试导航器新增了几项功能 可帮助你处理带标签的测试 默认情况下 导航器 按照测试在源代码中的 位置来显示测试 我一直忙着完善我的绝密辣酱配方 并希望扩大涉及它的代码的 测试覆盖范围 这些是我已经编写的测试 我将使用测试导航器底部的 筛选字段来查找 与我的辣酱相关的测试 当我开始输入内容时 Xcode 会根据 项目中的可用标签来进行推荐 Xcode 现在展示了几个 标签建议 比如 seasonal、 spicy 和 streetFood 我继续输入以缩小结果范围 默认情况下 筛选字段会与测试的 显示名称和函数名称进行匹配 这就是为什么会显示 Spice blend in gingerbread cookies 和 Spinach and artichoke dip ratio 等测试 Xcode 还会推荐名称中 没有突出显示任何字词的测试 这是因为它们的标签 与我输入的内容相匹配 当我点按推荐弹出窗口 中的“spicy”时 测试导航器会将我输入的内容 转换为标签筛选器 这会移除所有不带这个标签的测试 现在 测试导航器仅显示 带有 spicy 标签的测试 测试导航器还可以按标签 对测试进行分组 我可以通过点按 测试导航器顶部的 标签图标来切换到这个视图 我将移除标签筛选器 这样项目中的所有标签 都会显示在结果中 这个视图为我提供了一种便捷的方式 来运行测试 与层次结构视图一样 我可以点按任意标签旁的播放按钮 来运行包含这个标签的所有测试 在开发过程中 我可以运行与辣酱相关的测试 并快速获得关于更改的反馈 除了手动运行这些测试外 你可以使用重新设计的 测试计划编辑器 将这些 标签偏好设置保存到测试计划中 我已经制定了一个新的测试计划 其中包括一组可靠的核心测试 以便我可以在做出更改时 快速捕获可能出现的任何错误 这项测试计划包括 我的所有测试目标 我可以从测试导航器的 测试计划列表中选择 新测试计划的名称“Core Food” 从而切换到这个测试计划 然后 直接在测试导航器中 点按它的名称 即可打开测试计划编辑器 我将展开单元测试目标 来查看我的所有测试 我还会隐藏导航器 以便给自己留出更多的观察空间 提醒一下 测试计划可以 引用一个或多个测试目标 并且你可以借助测试计划编辑器 跨所有这些目标来组织测试
每个套件和测试的标签 均显示在右侧栏中
通过在测试计划编辑器顶部的 这些字段中指定标签 我可以选择添加或排除哪些测试 假设我想更新这个测试计划 以运行我的所有核心测试 带有 seasonal 标签的测试除外 这些测试涵盖的代码 只在一年中的特定时间运行 因此我不想让它们一直运行 我可以通过在排除字段中添加 seasonal 标签来排除 带有这个标签的测试
测试计划预览会根据 我的更改自动更新 包含字段和排除字段中 当前活跃的标签 将以紫色高亮显示 从测试计划中排除的标签 将被划掉 我要在排除字段中添加另一个标签 然后我就会看到 一些额外的筛选选项 如果我的测试计划 根据多个标签进行筛选 Xcode 会提供一个选项 可以选择匹配所有标签或任一标签 所有标签均为默认 在本例中 我将选择匹配任一标签 因为我想排除 所有带有 seasonal 或 unreleased 标签的测试 现在这两个标签都以紫色高亮显示 而且都被划掉了 这是因为系统主动把它们 排除在我的测试计划之外 标签也是一种实用工具 可以帮助你 在整个测试目标中分析结果 这是运行刚才制定的测试计划后 生成的测试报告 当然失败的案例也不在少数 因此让我们来深入探究一下 测试报告能如何帮助我们 利用标签更快地修复故障 我们来了解一下测试大纲屏幕 现在标签会出现在大纲中 位于对应的套件和测试旁边
我们可以使用标签筛选器 缩小结果范围 但因为我失败了太多次 要逐一查看会很繁琐
因此导航到包含所有 洞察信息的屏幕 其中有一个分发洞察信息的新版块 它可以显示 具有共同运行目标位置、 标签或错误的测试失败模式 这些洞察信息十分有趣 所有含 spicy 标签的测试 均以失败告终 我们连按两下洞察信息行 即可导航到详情屏幕
所有相关的失败测试 以及相应失败信息 均显示在这个屏幕上 我最近改良了秘制辣酱中 使用的辣椒 所以这可能就是 所有 spicy 测试都失败的原因 我会检查我的更改并修复测试
Xcode Cloud 也已经 更新为支持 Swift Testing 就像在 Xcode 中一样 你可以在 App Store Connect 的 Xcode Cloud 标签页中 查看测试套件的结果 其中包括测试中 已经定义的特征的详细信息 当你组织和关联测试时 Xcode 可以帮助你了解 对它们造成影响的问题 套件和标签使大型测试集合更易于 浏览和管理 接下来请 Jonathan 继续 谈一谈并行运行测试 现在 你已经有了一个规模可观、 易于管理的测试套件 是时候考虑如何通过并行测试 来确保你的测试快速通过 以及如何确保它们在 并发环境中可靠运行 Swift Testing 中 默认启用了并行测试 因此无需编写额外代码 就能开始充分利用上述功能 首次 你可以在所有物理设备上 运行并行测试 利用所有这些巨大的优势 轻松应对更多测试 我们先来了解一下 并行测试的基础知识 测试在串行测试中逐个运行 如果你曾使用过 XCTest 这就是它运行测试的默认方式 这与并行测试不同 并行测试可以并发执行 并行运行测试有几项优势
首先 执行时间将缩短 这对分秒必争的 CI 至关重要 这也意味着能更快地得到结果 在默认情况下 Swift Testing 以并行方式运行测试函数 无论是同步还是异步 这与 XCTest 有明显不同 后者只支持使用多个进程的并行化 每个进程一次运行一个测试 如有需要 测试函数可以孤立于 全局 actor 比如 MainActor 其次 测试运行的顺序是随机的 这有助于发现测试之间 隐藏的依赖关系 并显现出可能需要调整的地方 我们来看一个例子 我有两个测试 第一个测试中 我烤了一个纸杯蛋糕 在第二个测试中 我吃了它 如果这些测试始终按顺序逐个运行 那么我会始终准备好一个纸杯蛋糕 以进行第二次测试 因为它在第一次测试中就烤好了 这是不合理的 如果测试并行运行 那么第二个测试对第一个测试的 依赖关系就会在运行时显现出来 我可以解决这个问题 如果要转换旧版的测试代码 其中一些依赖关系可能已经内置 在重写现有代码时 Swift 6 可以帮助你 发现其中的一些问题 但其他问题则较难发现 首先 你可能只想将代码 转换为 Swift Testing 然后再回来解决这些问题 你可能还无法全部解决这些问题 这正是 .serialized 特征 可以提供帮助的地方 可将 .serialized 特征 添加到测试套件中 以表明测试需要串行运行 这些测试将失去 我们刚才讨论过的优势 因此你首先考虑的应是 尽可能重构测试代码 使它们并行运行 .serialized 也可 应用于参数化测试函数 以确保测试用例一次运行一个 如果应用于包含另一个套件的套件 则会自动继承 无需重复添加 具有 .serialized 特征的 套件中的测试将逐个运行 不过 Swift 仍然可以自由 利用这些序列化测试 来并行运行其他无关的测试 因此你仍然可以获得并行性能 如有必要 你可以按顺序运行测试 但我们建议重构测试 以便测试能并行运行 使用 Swift Testing 时 并行测试默认处于开启状态 它能让测试尽可能快地运行 Swift 6 还能帮助你 发现测试中 妨碍并行运行的问题 接下来 我们将向大家展示 使用 Swift Testing 等待异步条件的技巧 编写并发测试代码时 你可以在 Swift 中使用 与生产代码相同的并发功能 await 的工作方式完全相同 它会挂起一个测试 允许其他测试代码 在等待期间继续使用 CPU 一些代码 尤其是用 C 语言或 Objective-C 语言编写的旧版代码 会使用完成处理程序 来发出异步操作结束的信号 这段代码将在测试函数 返回之后运行 并且你无法验证函数是否成功 对于大多数完成处理程序 Swift 会自动提供异步重载 供你使用 如果要测试的代码 使用了完成处理程序 而异步重载不可用 你可以改用 withCheckedContinuation 或 withCheckedThrowingContinuation 将它转换为可等待的表达式
如需进一步了解 Swift 中的 延续性功能 请观看 “认识 Swift 的 async/await”
另一种回调是可能触发 多次的事件处理程序 这个版本的 eat 函数 会为每块饼干调用一次回调 而不是在整个用餐过程结束时调用 不过在 Swift 6 中 尝试用变量 来计算吃掉的饼干数量 会导致并发错误 因为这样设置变量是不安全的
如果要测试的代码 可能会多次调用某个回调 并且你需要测试调用的次数 则可以改用确认 默认情况下 确认只能发生一次 但你可以指定其他预期计数 我正在做烘焙 并吃了 10 块美味的饼干 所以我预计这个事件 会发生 10 次 如果在测试期间永远不会进行确认 你也可以指定 0 次 Swift 并发是生产代码 和测试中的一个强大工具 并行运行测试可更快获得结果 使用 async/await、 延续性功能和确认功能 可确保测试代码 在并发环境中正确运行 在本次讲座中 大家学习了 Swift Testing 改进测试流程的方法 我们讨论了各种主题 现在我们来快速回顾一下 首先 我们了解了 Swift Testing 的 API 如何帮助编写极具表达力的测试 借助参数化 你可以利用单个测试 来练习许多不同的用例 套件和标签等工具可帮助你 整理和记录测试代码 最后 并行测试可以 缩短运行测试的时间 而且可能有助于识别 测试之间的依赖关系 感谢观看本次讲座 祝各位测试顺利
-
-
0:01 - Successful throwing function
// Expecting errors import Testing @Test func brewTeaSuccessfully() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) let cupOfTea = try teaLeaves.brew(forMinutes: 3) }
-
0:02 - Validating a successful throwing function
import Testing @Test func brewTeaSuccessfully() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) let cupOfTea = try teaLeaves.brew(forMinutes: 3) #expect(cupOfTea.quality == .perfect) }
-
0:03 - Validating an error is thrown with do-catch (not recommended)
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 3) do { try teaLeaves.brew(forMinutes: 100) } catch is BrewingError { // This is the code path we are expecting } catch { Issue.record("Unexpected Error") } }
-
0:04 - Validating a general error is thrown
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect(throws: (any Error).self) { try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test! } }
-
0:05 - Validating a type of error
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect(throws: BrewingError.self) { try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test! } }
-
0:06 - Validating a specific error
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect(throws: BrewingError.oversteeped) { try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test! } }
-
0:07 - Complicated validations
import Testing @Test func brewTea() { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect { try teaLeaves.brew(forMinutes: 3) } throws: { error in guard let error = error as? BrewingError, case let .needsMoreTime(optimalBrewTime) = error else { return false } return optimalBrewTime == 4 } }
-
0:08 - Throwing expectation
import Testing @Test func brewAllGreenTeas() { #expect(throws: BrewingError.self) { brewMultipleTeas(teaLeaves: ["Sencha", "EarlGrey", "Jasmine"], time: 2) } }
-
0:09 - Required expectations
import Testing @Test func brewAllGreenTeas() throws { try #require(throws: BrewingError.self) { brewMultipleTeas(teaLeaves: ["Sencha", "EarlGrey", "Jasmine"], time: 2) } }
-
0:10 - Control flow of validating an optional value (not recommended)
import Testing struct TeaLeaves {symbols let name: String let optimalBrewTime: Int func brew(forMinutes minutes: Int) throws -> Tea { ... } } @Test func brewTea() throws { let teaLeaves = TeaLeaves(name: "Sencha", optimalBrewTime: 2) let brewedTea = try teaLeaves.brew(forMinutes: 100) guard let color = brewedTea.color else { Issue.record("Tea color was not available!") } #expect(color == .green) }
-
0:11 - Failing test with a throwing function
import Testing @Test func softServeIceCreamInCone() throws { try softServeMachine.makeSoftServe(in: .cone) }
-
0:12 - Disabling a test with a throwing function (not recommended)
import Testing @Test(.disabled) func softServeIceCreamInCone() throws { try softServeMachine.makeSoftServe(in: .cone) }
-
0:13 - Wrapping a failing test in withKnownIssue
import Testing @Test func softServeIceCreamInCone() throws { withKnownIssue { try softServeMachine.makeSoftServe(in: .cone) } }
-
0:14 - Wrap just the failing section in withKnownIssue
import Testing @Test func softServeIceCreamInCone() throws { let iceCreamBatter = IceCreamBatter(flavor: .chocolate) try #require(iceCreamBatter != nil) #expect(iceCreamBatter.flavor == .chocolate) withKnownIssue { try softServeMachine.makeSoftServe(in: .cone) } }
-
0:15 - Simple enumerations
import Testing enum SoftServe { case vanilla, chocolate, pineapple }
-
0:16 - Complex types
import Testing struct SoftServe { let flavor: Flavor let container: Container let toppings: [Topping] } @Test(arguments: [ SoftServe(flavor: .vanilla, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .chocolate, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .pineapple, container: .cup, toppings: [.whippedCream]) ]) func softServeFlavors(_ softServe: SoftServe) { /*...*/ }
-
0:17 - Conforming to CustomTestStringConvertible
import Testing struct SoftServe: CustomTestStringConvertible { let flavor: Flavor let container: Container let toppings: [Topping] var testDescription: String { "\(flavor) in a \(container)" } } @Test(arguments: [ SoftServe(flavor: .vanilla, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .chocolate, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .pineapple, container: .cup, toppings: [.whippedCream]) ]) func softServeFlavors(_ softServe: SoftServe) { /*...*/ }
-
0:18 - An enumeration with a computed property
extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio var containsNuts: Bool { switch self { case .rockyRoad, .pistachio: return true default: return false } } } }
-
0:19 - A test function for a specific case of an enumeration
import Testing @Test func doesVanillaContainNuts() throws { try #require(!IceCream.Flavor.vanilla.containsNuts) }
-
0:20 - Separate test functions for all cases of an enumeration
import Testing @Test func doesVanillaContainNuts() throws { try #require(!IceCream.Flavor.vanilla.containsNuts) } @Test func doesChocolateContainNuts() throws { try #require(!IceCream.Flavor.chocolate.containsNuts) } @Test func doesStrawberryContainNuts() throws { try #require(!IceCream.Flavor.strawberry.containsNuts) } @Test func doesMintChipContainNuts() throws { try #require(!IceCream.Flavor.mintChip.containsNuts) } @Test func doesRockyRoadContainNuts() throws { try #require(!IceCream.Flavor.rockyRoad.containsNuts) }
-
0:21 - Parameterizing a test with a for loop (not recommended)
import Testing extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio } } @Test func doesNotContainNuts() throws { for flavor in [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip] { try #require(!flavor.containsNuts) } }
-
0:22 - Swift testing parameterized tests
import Testing extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio } } @Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip]) func doesNotContainNuts(flavor: IceCream.Flavor) throws { try #require(!flavor.containsNuts) }
-
0:23 - 100% test coverage
import Testing extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio } } @Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip]) func doesNotContainNuts(flavor: IceCream.Flavor) throws { try #require(!flavor.containsNuts) } @Test(arguments: [IceCream.Flavor.rockyRoad, .pistachio]) func containNuts(flavor: IceCream.Flavor) { #expect(flavor.containsNuts) }
-
0:24 - A parameterized test with one argument
import Testing enum Ingredient: CaseIterable { case rice, potato, lettuce, egg } @Test(arguments: Ingredient.allCases) func cook(_ ingredient: Ingredient) async throws { #expect(ingredient.isFresh) let result = try cook(ingredient) try #require(result.isDelicious) }
-
0:26 - Adding a second argument to a parameterized test
import Testing enum Ingredient: CaseIterable { case rice, potato, lettuce, egg } enum Dish: CaseIterable { case onigiri, fries, salad, omelette } @Test(arguments: Ingredient.allCases, Dish.allCases) func cook(_ ingredient: Ingredient, into dish: Dish) async throws { #expect(ingredient.isFresh) let result = try cook(ingredient) try #require(result.isDelicious) try #require(result == dish) }
-
0:28 - Using zip() on arguments
import Testing enum Ingredient: CaseIterable { case rice, potato, lettuce, egg } enum Dish: CaseIterable { case onigiri, fries, salad, omelette } @Test(arguments: zip(Ingredient.allCases, Dish.allCases)) func cook(_ ingredient: Ingredient, into dish: Dish) async throws { #expect(ingredient.isFresh) let result = try cook(ingredient) try #require(result.isDelicious) try #require(result == dish) }
-
0:29 - Suites
@Suite("Various desserts") struct DessertTests { @Test func applePieCrustLayers() { /* ... */ } @Test func lavaCakeBakingTime() { /* ... */ } @Test func eggWaffleFlavors() { /* ... */ } @Test func cheesecakeBakingStrategy() { /* ... */ } @Test func mangoSagoToppings() { /* ... */ } @Test func bananaSplitMinimumScoop() { /* ... */ } }
-
0:30 - Nested suites
import Testing @Suite("Various desserts") struct DessertTests { @Suite struct WarmDesserts { @Test func applePieCrustLayers() { /* ... */ } @Test func lavaCakeBakingTime() { /* ... */ } @Test func eggWaffleFlavors() { /* ... */ } } @Suite struct ColdDesserts { @Test func cheesecakeBakingStrategy() { /* ... */ } @Test func mangoSagoToppings() { /* ... */ } @Test func bananaSplitMinimumScoop() { /* ... */ } } }
-
0:31 - Separate suites
@Suite struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:32 - Separate suites
@Suite struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:35 - Using a tag
import Testing extension Tag { @Tag static var caffeinated: Self } @Suite(.tags(.caffeinated)) struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test(.tags(.caffeinated)) func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:36 - Declare and use a second tag
import Testing extension Tag { @Tag static var caffeinated: Self @Tag static var chocolatey: Self } @Suite(.tags(.caffeinated)) struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test(.tags(.chocolatey)) func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test(.tags(.caffeinated, .chocolatey)) func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:37 - Two tests with an unintended data dependency (not recommended)
import Testing // ❌ This code is not concurrency-safe. var cupcake: Cupcake? = nil @Test func bakeCupcake() async { cupcake = await Cupcake.bake(toppedWith: .frosting) // ... } @Test func eatCupcake() async { await eat(cupcake!) // ... }
-
0:38 - Serialized trait
import Testing @Suite("Cupcake tests", .serialized) struct CupcakeTests { var cupcake: Cupcake? @Test func mixingIngredients() { /* ... */ } @Test func baking() { /* ... */ } @Test func decorating() { /* ... */ } @Test func eating() { /* ... */ } }
-
0:39 - Serialized trait with nested suites
import Testing @Suite("Cupcake tests", .serialized) struct CupcakeTests { var cupcake: Cupcake? @Suite("Mini birthday cupcake tests") struct MiniBirthdayCupcakeTests { // ... } @Test(arguments: [...]) func mixing(ingredient: Food) { /* ... */ } @Test func baking() { /* ... */ } @Test func decorating() { /* ... */ } @Test func eating() { /* ... */ } }
-
0:40 - Using async/await in a test
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await eat(cookies, with: .milk) }
-
0:41 - Using a function with a completion handler in a test (not recommended)
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) // ❌ This code will run after the test function returns. eat(cookies, with: .milk) { result, error in #expect(result != nil) } }
-
0:42 - Replacing a completion handler with an asynchronous function call
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await eat(cookies, with: .milk) }
-
0:43 - Using withCheckedThrowingContinuation
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await withCheckedThrowingContinuation { continuation in eat(cookies, with: .milk) { result, error in if let result { continuation.resume(returning: result) } else { continuation.resume(throwing: error) } } } }
-
0:44 - Callback that invokes more than once (not recommended)
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) // ❌ This code is not concurrency-safe. var cookiesEaten = 0 try await eat(cookies, with: .milk) { cookie, crumbs in #expect(!crumbs.in(.milk)) cookiesEaten += 1 } #expect(cookiesEaten == 10) }
-
0:45 - Confirmations on callbacks that invoke more than once
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await confirmation("Ate cookies", expectedCount: 10) { ateCookie in try await eat(cookies, with: .milk) { cookie, crumbs in #expect(!crumbs.in(.milk)) ateCookie() } } }
-
0:46 - Confirmation that occurs 0 times
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await confirmation("Ate cookies", expectedCount: 0) { ateCookie in try await eat(cookies, with: .milk) { cookie, crumbs in #expect(!crumbs.in(.milk)) ateCookie() } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。