大多数浏览器和
Developer App 均支持流媒体播放。
-
编写不合格的测试
为不合格测试制定计划:设计良好的测试,从而帮助你发现和诊断最棘手的漏洞。了解如何使用 XCTest 找到最优秀的代码中的隐藏问题,进而改进自动化测试。 我们将介绍如何进行不合格测试,从而简化分类问题,让你解决界面问题并快速提供修复程序。 要想充分利用本节内容,建议你先熟悉在 XCTest 框架内编写 UI 测试。 获取更多有关测试工具的信息,请查看“套件的测试周期”。
资源
相关视频
WWDC22
WWDC20
-
下载
(你好 WWDC 2020) 你好 欢迎来到 WWDC
(工程经理) 你好 欢迎收看 “编写不合格的测试” 我是 Kelly Keenan 在这个会话中 我会分享一些我在过去的几年中学到的 关于编写用户界面以及 Xcode 综合测试 的经验教训 虽然我的分享会集中在用户界面上 但是许多这些经验教训 也可以适用于单元测试 无论我是在编码之前或是之后编写测试 我主要的动机都是让我们的测试合格 因为看到了这个图标就代表了 我的测试通过了 也代表着我可以继续推进我的产品了 然而 今年我的新目标是 编写不合格的测试 由于好的测试可以检测漏洞 而我们应该为失败作出计划 测试是一次就写完的 但是却要被筛选许多次 当我的测试检测到了一个产品漏洞 它就会失败 而这也正是测试的设计用途 在我的例子中 所有测试都会在一个持续集成系统中运行 也因此 我用来筛选缺陷的工具 是这个测试的结果 bundle 在这个会话中 我们会探索我找到的 可以使我的测试 用结果 bundle 更容易地筛选错误的方法 以及使我的测试更加稳健的方法 以便我将时间花费在筛选产品缺陷上 而不是调试我的测试上 测试模版遵循 组建、测试、推翻的模式 在测试板块中 我们可以将它拆分成 动作和断言 我们也将使用这个作为这次会话的议程 那么 让我们从测试的组建开始吧 在测试组建的部分 我会明确地陈述我需要的设想 并且在运行我的测试之前 设置好我的 app 和环境的状态 在 Xcode 11.4 中 我们介绍了 一个全新的组建功能 叫做 setUpWithError throws 语句 使我可以在组建过程中 捕捉或终止任何被抛掷的错误 我发现这对于现代化我已有的组建方法 十分有用 并以此来利用错误管理 我使用 setUpWithErrors 方法 来在我的测试运行之前 设定它们要求的初始状态 因为之前的测试也许会 改变我的 app 的状态 或者修改我的测试使用的数据
在这个例子中 我将 continueAfterFailure 设置为错误的 以便当一个问题被找到时 我的测试可以立即失败 这帮助我可以更快地找到第一处错误 而不是艰难地徘徊在好几处错误之中 我还将此当作一个 在这一类的每一个测试中 运行这个 app 的机会 我所涉及的一个技巧是 利用启动参数和环境变量 来快速地设置我的 app 的状态 这种技巧不应该被使用来设置所有的状态 但也有一些情况是需要它的 比如 在测试中绕过双因素认证 在这个例子中 我使用它作为 一个绕过菜单标签的方法 并取而代之地 从食谱标签开始 像这样的小的改变 可以通过避免不必要的工作 改善运行测试的速度 而且更重要的是 当我期待看到食谱标签测试结果时 它可以避免或许会在菜单标签中发生的 筛选缺陷 总而言之 我使用 setUpWithError throws 语句 来增强错误处理能力 我在这一类中 对每个测试都执行了 常见的组建任务 比如运行 app 我使用启动参数来与 app 沟通 以设置其状态 我也采用产品更改 来快速地设置状态并专注于测试 下一步就是运行我们的测试 测试应该首先专注于执行一个动作 然后再主张那个动作被完成了 让我们从我如何使我的操作 更简单地用于筛选开始吧 我考虑的第一件事是 每一个测试都应该有一个特定的目标 这个目标应该在测试的标题中 就被反映出来 在这个例子中 我正在测试食材列表的准确性 我的测试唯一需要执行的操作就是 选择 Berry Blue 食谱 尽量减少操作使之后筛选失败更为容易
轻点这一行会显示出食谱 并且由于我的操作 我也可以核实食材列表 同样地 在我的结果 bundle 中 多亏了我的测试名字 我可以很轻易地看到 我的测试正在验证什么内容 讲到命名 在过去的几年中 我发现用户界面的元素标签总是变化 所以作为预防措施 我对所有的字符串值都使用枚举 这样 当用户界面改变时 可以轻易地更新我的测试 以响应那些改变 这不仅仅节省了我的时间 使我能够根据用户界面更改更新我的测试 也使得我接触的由于难以辨别的拼写错误 而导致的一次测试的失败次数达到了最少 就像收集所有的字符串到枚举中一样 另一个我用来最小化错误的方式是 通过把公共代码分解成辅助函数 因此多个测试可以使用同一个代码路径 在这个例子中 多个测试需要 访问在 app 中的思慕雪列表 并选择一份食谱 选择这个公共测试路径 意味着除了复制代码 我也可以花费我的精力 在巩固这些路径上并以此减少测试错误 另一个我使用的技巧是 塑造我的 app 的域名 并围绕该域名设计一个测试语言 然后我的测试就会显示出我的 app 语言 在这个例子中 我可以向 Fruta app 索取思慕雪列表 然后我可以在我的思慕雪列表上 执行一个操作 比如说选择一份食谱 这会返还食谱用户界面元素 这些都基于 我创造来追踪 app 以及低级元素的 FrutaUIElement 类的基础上 这样 我就能够使我的共享代码 差不多是面向对象的 测试非常实用 且基于元素和查询 我可以为可读性模拟一个面向对象的环境 这么一来 我能够访问我的测试 映射到我如何看待我的 app 作为一系列子视图 使用这个模型的结果 就是我在与每个简化层级的元素打交道 并且我也可以使我的查询 只专注于该元素的子单元 在过去的几年 我们的共享测试代码 变得很是庞大 所以 为了处理这个情况 我们便向对待产品代码一样对待它 并且为我们的测试创造了一个共享框架 你也可以考虑使用 Swift 套件 来分享你的测试代码 尤其当你在多个 app 之间分享代码 总结一下这个部分 我为了一个具体的目标设计测试 以此来专注于我测试的内容 我使用枚举并将公共代码 分解成为辅助函数 来简化解决用户界面更改 我在我的测试中建模对象 以此来反应我的 app 的用户界面层级 我使用框架 或者你也可以用 Swift 套件 来在项目之间分享代码 现在让我们来看一看我最喜欢的部分 测试断言 因为这是我们真正开始 着手于测试的核心部分 这里是一些我在测试断言和错误处理中 学到的经验教训 它们使得测试故障能够容易地被筛选出来 对我帮助很大的一件小事是 在 XCTAssert 函数中使用非必须的信息 当我在我的 desk 筛选测试时 不留言是可以的 但是当我只有结果 bundle 时 就少了很多上下文 在这个例子中 我知道三不等于二 但是是两个什么? 如果我添加一条信息 就意味着我可以添加上下文 人们大多数时候都在读我的断言信息 而这个人总是我 所以我喜欢给我自己留一条线索 来解释为什么这个表达失败了 然而 有些时候 我的断言故障 是被自动化系统读取的 在这种情况下 我希望我的信息能够具体 但不要太具体 所以 我留出像日期/时间戳这类的东西 或者特殊的文件通道 以便断言信息可以在多个 因为同一原因失败的测试中 被识别出 我还会尝试确保我对所想达到的目标 使用了正确的断言 这么做确保了 当测试失败时 我所看见的自动信息更加相关 在 Xcode 12 中 我们添加了 XCTIssue 它是一个全新的低级别报告故障方式 如果你想了解更多信息 可以观看相关视频 “ 通过 XCTIssue 筛选测试故障 “ 我遇到的关于断言的陷阱之一是异步事件 我有时会在筛选异步事件时遇到困难 在这个例子中 我轻点了食谱按钮 但是根据我的代码正在做的事情不同 它也许需要一点时间才能够加载完成 如果我立即返回到食谱元素 它也许还没有生成 在过去 我试过使用休眠方法 来给我的测试一点内置时间 然而我不会在工作室睡觉 那么为什么要让我的测试这么做呢 再者说 这耽搁了我更快地拿到测试结果 XCTest 用于内置重试 但是根据我的 app 代码 它也许不够充分 所以我更喜欢使用 waitForExistence 和暂停 这提供了轮询 所以如果期望值在暂停之前为真 那么我就节省了那么多等待的时间 它也使我的测试可以 在一个我设计的环境下 明确地通过或者失败 在结果 bundle 中 我可以看到我的测试等待了五秒钟 来搜索食材视图 另一个建议是展开可选项 在这个例子中 我想要返回在传入的字符串数组中的 收藏夹计数 然而我没有花时间展开可选项 当我在本地运行代码时 这导致了一个崩溃并终止了我的测试 如果我运行了代码 而我去吃午饭但等我回来代码却还没完成 这将是真的太倒霉了 当这种情况 在一个持续集成的环境下发生时 我会使用一个结果 bundle 以及一个可读的失败测试 “ 因信号错误而导致的崩溃 ” 我可以通过确保我展开了我的可选项 来轻易地避免这种情况 我可以使用由 Swift 提供的 展开可选项方法 例如 “ if let ” 而我也能接着在 if 块中使用这个展开值 如果你想要在之后使用展开变量 我可以使用“ guard let ” 它使我能够在 guard 块中 抛出一个我提供的错误 如果我遇到空值
第三个方案是使用空合运算符 如果我遇到空值的话 它使我能够提供一个缺省值 而在这个例子中 则是一个数组空值 第四个方案是使用 XCTUnwrap 它是由 XCTest 框架提供的 它是简化版的“ guard let ” 如果我的测试遇到空值 它就会抛出一个错误 使用 XCTUnwrap 显示了我在访问中的评论 以及在我的结果 bundle 中 自动生成的信息 关于展开可选项最棒的部分是 通过循序渐进地失败 而不是崩溃 tearDown 方法将会被访问 讲到循序渐进地失败 让我们也来聊一聊抛出错误
在我的测试中 原则是在共享代码中 我总是抛掷而不是断言 这么做的理由是因为共享代码 在很多测试中都被运行 并且这其中的一些测试中 我也许特意测试了负面测试案例 来保证隐藏的一些东西不被显示出来 或者出于测试目的 强制显示错误对话框 所以 在像这样的案例中 我有一个共享方法来核实成分 我也许会在我有之前显示的 额外成分的情况下 测试一个漏洞 并且我会测试这些不再显示的额外成分 所以 我会抛出一个错误 在我的错误中 我经常传入要在我的错误描述中显示的值 这是 CustomStringConvertible 协议 的一个必要条件 使用描述函数意味着在结果 bundle 中的 我没有在本地筛选结果的次数中 我可以看到与上下文更加相关的错误 如果我在本地筛选 那么 Xcode 12 的新功能 会使错误回溯直接在我的代码中被看到 因此我不用再困惑 在我的共享代码中错误到底藏在哪里 我也可以在 Runtime Issues Navigator 找到一个回溯 以及结果 bundle 想要了解更多关于如何利用测试回溯 可以观看相关视频 “通过 XCTIssue 筛选测试故障”
在这个结果 bundle 中 还有一个用户可读的披露组 它由我的代码添加来提供更多关于 当时我在执行什么操作的上下文 在这 我可以轻易地看到 我在 Berry Blue 思慕雪中找 “葡萄” 是绝对错误的 为提供这样的网页结构导航 我使用 XCTContext.runActivity 并提供了一个名称 这就是在结果 bundle 中显示出来的 以及在它的块中执行的操作 这是一个非常棒的方式 来添加一些组织和上下文 到我的结果 bundle 中 并根据测试执行的动作 使它容易被读取 另一件我可以对 runActivity 做的事是 通过 XCTAttachment 添加附件 我可以添加像文件、图像、数据 这类的附件 到我的 XCTContext 或测试例子中 然后他会在一个结果 bundle 中显示出来 这是一个为失败测试 收集额外的 logging 模块的很好的方式 尤其是当它来自于 CI 系统 早些时候 我提到了不要将文件通道 添加到断言注释中 因为 相反地 我可以将文件路径 和文件本身全部作为附件添加
这使得之后筛选失败更加容易
我相信测试应该负责 所有在筛选产品失败过程中所需的数据 因为这些数据之后也许无法使用了 有些时候 一个测试完全不应该被运行 在这种情况下 我会使用 XCTSkip XCTSkipUnless 以及 XCTSkipIf 来记录 没有通过添加非必须信息运行的测试 其主要作用是跳过与测试在运行的 平台不相关的测试 我在实践中使用的一些替代方法是 我想为一个新特性写的临时测试 它会允许我看到哪些测试是未执行的 而哪些测试是退化的 第三点是有时候有些测试由于多种原因 暂时就是 不能被修复的 我不想继续筛选失败 但我也不想因为禁用它 而失去对测试的追踪 使用 XCTSkip 允许我继续 在结果 bundle 中看到跳过的测试 因此我不会忘记我需要编写这个测试 或者当问题被解决时修改这个测试
总结一下这个部分 我喜欢添加断言信息 并使用相关的 XCTAssert 函数 来在我的结果 bundle 中 为失败添加上下文 我一定会展开可选项 来确保我去吃午餐时 我的测试不会崩溃 并且我的 tearDown 方法 也会被访问 我对异步事件以及时间问题 使用 waitForExistence 方法 而不是休眠方法 我在共享代码中抛出错误而不是断言 以便使用这个代码的其他测试 可以为负面测试捕捉到错误 我使用 XCTContext.runActivity 以及附件 来为我的结果 bundle 添加更多上下文和内容 我将 XCTSkip 用于在当前的方案中 不希望被运行的测试 最后 让我们看一看推翻 由于我的大多数工作已经完成了 我对于推翻只有三点建议 第一点是我已经现代化了我的测试 使用可抛掷的 tearDownWithError 来利用新的错误管理 我使用 tearDown 方法作为一个时间点 来收集额外的 logging 块 包括一些对于失败的分析 这就是我重置 在组建时所做的更改的环境的时候 总结一下这个视频 我们浏览了组建 也就是我改变环境 并确认我的测试所需的假定
测试操作是通过 模仿我的 app 共享代码 执行我想要测试的必要操作的地方 接着 通过使用 辅助方法、错误以及测试断言 我核实操作得以正确完成 之后我用 tearDown 方法完成收集数据 以及测试后的清理工作 我希望这些技巧和建议 可以使你的测试更稳健 并且可以轻松快速地筛选出你的产品问题 以便你的测试可以通过 而你也可以推出一款优质产品 谢谢
-
-
1:58 - Use setUpWithError()
class RecipesTests: XCTestCase { let app = FrutaApp() override func setUpWithError() throws { continueAfterFailure = false app.launchArguments.append("-recipes-tests") app.launch() } }
-
3:09 - Use launch arguments
class RecipesTests: XCTestCase { let app = FrutaApp() override func setUpWithError() throws { continueAfterFailure = false app.launchArguments.append("-recipes-tests") app.launch() } } @State private var selection: Tab = CommandLine.arguments.contains("-recipes-tests") ? .recipes : .menu
-
4:12 - Design tests for a specific goal
func testIngredientsListAccuracy() throws { // Select Berry Blue recipe let recipe = try app.smoothieList().selectRecipe (smoothie: .berryBlue) // Verify ingredients list try recipe.verify(ingredients: SmoothieType.berryBlue.ingredients) }
-
4:56 - Use enums for string values
public enum SmoothieType : String { case berryBlue = "Berry Blue" case carrotChops = "Carrot Chops" case berryBananas = "That's Berry Bananas!" var ingredients : [String] { switch self { case .berryBlue: return ["Orange", "Blueberry", "Avocado"] case .carrotChops: return ["Orange", "Carrot", "Mango"] case .berryBananas: return ["Almond Milk", "Banana", "Strawberry"] } } }
-
5:25 - Factor common code
let recipe = try app.smoothieList().selectRecipe(smoothie: .berryBlue) public class FrutaApp : XCUIApplication { public func smoothieList() throws -> SmoothieList { let element = tables["Smoothie List"] if !element.waitForExistence(timeout: 5) { throw FrutaError.elementDoesNotExist("Smoothie List table") } return SmoothieList(app: self, element: element) } } public class SmoothieList : FrutaUIElement { public func selectRecipe(smoothie: SmoothieType) throws -> Recipe { element.buttons[smoothie.rawValue].tap() return try app.recipe() } }
-
5:49 - Model UI hierarchy in testing code
public class FrutaApp : XCUIApplication { public func smoothieList() throws -> SmoothieList {  } } public class SmoothieList : FrutaUIElement { public func selectRecipe(smoothie: SmoothieType) throws -> Recipe {  } } open class FrutaUIElement { let app: FrutaApp let element: XCUIElement init(app: FrutaApp, element: XCUIElement) { self.app = app self.element = element } }
-
8:17 - Use assertion messages
XCTAssertEqual(count, expectedCount, "\(SmoothieType.berryBlue.rawValue) smoothie is expected to have \(expectedCount) ingredients: \(expectedIngredients), however, there were \(count) found.")
-
9:21 - Asynchronous events
public func selectRecipe(smoothie: SmoothieType) throws -> Recipe { element.buttons[smoothie.rawValue].tap() return try app.recipe() } public func recipe() throws -> Recipe { let element = scrollViews["Ingredients View"] if !element.waitForExistence(timeout: 5) { throw FrutaError.elementDoesNotExist( "Ingredients View scroll view") } return Recipe(app: self, element: element) }
-
10:19 - Unwrapping optionals
func countFavorites(favorites: [String]?) -> Int{ let favs = favorites! return favs.count }
-
10:56 - Unwrapping optionals continued
if let favs = favorites {  } guard let favs = favorites else { /* throw an error */ } let favs = favorites ?? [] let favs = try XCTUnwrap(favorites, "favorites is nil, so there is nothing to count”)
-
12:19 - Throw errors from shared code
public func verify(ingredients: [String]) throws { try XCTContext.runActivity(named: "Verifying \(ingredients) exists in the Recipe screen.") { verifyingRecipe in for ingredient in ingredients { if !element.switches[ingredient].waitForExistence(timeout: 5) { throw RecipeError.ingredientDoesNotExist(ingredient) } } } } public enum RecipeError : Error, CustomStringConvertible { case ingredientDoesNotExist(String) public var description : String { switch self { case .ingredientDoesNotExist(let ingredient): return "\(ingredient) does not exist in the Ingredients View.)" } } }
-
13:41 - Use XCTContext.runActivity()
public func verify(ingredients: [String]) throws { try XCTContext.runActivity(named: "Verifying \(ingredients) exists in the Recipe screen.") { verifyingRecipe in for ingredient in ingredients { if !element.switches[ingredient].waitForExistence(timeout: 5) { throw RecipeError.ingredientDoesNotExist(ingredient) } } }
-
14:02 - Add attachments to the result bundle
public func verify(ingredients: [String]) throws { try XCTContext.runActivity(named: "Verifying \(ingredients) exists in the Recipe screen.") { verifyingRecipe in for ingredient in ingredients { if !element.switches[ingredient].waitForExistence(timeout: 5) { let attachment = XCTAttachment(string: element.debugDescription) verifyingRecipe.add(attachment) throw RecipeError.ingredientDoesNotExist(ingredient) } } }
-
14:50 - Use XCTSkip
let debuggingTests = false func testSelectSmoothie() throws { try XCTSkipUnless(debuggingTests == true, "This test is not yet implemented.") }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。