掌握XCTestExpectation的4个技巧
我相信,使我们成为优秀的程序员的一种愿望就是掌握和改进我们所有人拥有的工具集。 考虑到这一点,在华沙BlaBlaCar办公室的一个闪亮的早晨,我停了一会儿,看看如何才能从旧的XCTestExpectation
更多XCTestExpectation
。 在本文中,我将向您展示一些现在可以用来更有效地编写单元测试的内容。
这4个技巧可用于测试异步代码。 我将用来自不同领域的实际例子来说明我的想法。
有时有必要测试给定的异步代码是否多次执行。 .expectedFulfillmentCount
使它变得不那么容易。 它表示在完全满足期望之前必须调用满次fulfill()
的次数。
示例:考虑一个可以通过调用execute()
启动的Task
对象。 它将状态更新报告给委托对象。 我们要测试的是,一旦任务开始,它的状态就异步地变为downloading
, processing
并最终finished
:
MockTaskStatusDelegate
执行didCall_taskDidChangeStatus
块以确认调用了适当的委托方法。 在此结束语(第21行)中, “代表被称为3次”得以实现。 我们使用recordedStatuses
辅助数组来跟踪状态历史recordedStatuses
,并在期望完成后断言其正确性。
若要测试未执行给定的代码,可以使用.isInverted
属性。 如果满足,“反向”期望将失败。 在测试互斥流或仅在给定的事情应该在一种配置下发生而不在另一种配置下发生时,它很有用。
示例:假设我们构建了一个游戏,特别是关卡选择屏幕。 某些级别可用,而其他级别则被锁定 。 轻按第一组中的一个按钮即可开始播放,轻按上一个按钮则无济于事-这是我们要测试的行为。
从技术上讲,该屏幕由LevelSelectionViewModel
表示。 当用户点击按钮时,它公开从视图调用的selectLevel(atIndex:)
方法。 游戏开始时,应使用didRequestOpeningLevel(withIdentifier:)
通知didRequestOpeningLevel(withIdentifier:)
:
与前面的示例相似,当调用委托方法时, MockLevelSelectionViewModelDelegate
执行didCall_didRequestOpeningLevelWithIdentifier
块。 视图模型配置有两个级别: .unlocked
和.locked
,分别存储在索引0
和1
。 第一个单元测试模拟在索引0
上的敲击,并断言满足了期望。 第二个测试将期望值配置为反转(第38行),因此只有在给定的超时时间内未执行第41行时,期望才会通过。
值得一提的是,如果与第一个测试相辅相成,第二个测试将更有价值。 第一个测试确保视图模型与其委托人对话,而第二个测试则将此逻辑限制为仅.unlocked
按钮。 只有后者会给我们一种错误的正确感。 例如,想象一下, LevelSelectionViewModel
中存在一个错误,导致永远不会调用该委托。 第二项测试将通过(无法满足反向期望)。 没有第一个测试,我们可能会认为一切都很好,但事实并非如此。 在这样的示例中,第一个测试将失败,因为它的期望无法实现。
在测试方法的末尾不一定必须验证期望。 可以使用以下方便的XCTestCase函数来完成此操作:
func wait(for: [XCTestExpectation], timeout: TimeInterval, enforceOrder: Bool)
—等待一系列期望值,并指定是否必须按照给定的顺序实现它们。
如果测试运行wait(for:timeout:)
在执行测试方法时调用wait(for:timeout:)
,则它将阻止执行并仅在满足所有给定的期望后才继续执行 。 如果使用enforceOrder
标志,它也可以保证订单。
示例1:只有在存储解锁后才能在SecureStorage
更改机密。 我们要测试从存储中读取的内容是以前保存的内容。
因为unlock(completion:)
方法是异步完成的,所以我们需要等待它再调用save()
。 现在,由于保存也是异步操作,因此我们要等到保存完成后再调用read()
。
测试执行将首先在第12行暂停,直到在storage.unlock()
调用完成块。 然后将其恢复,调用save()
并在第18行中等待其异步结果。最后, readExpectation
将阻止测试方法返回,直到执行read()
完成块为止。
示例2 :让我们回到“实现数量”示例。 我们使用了一个数组来存储Task
状态值并声明一个顺序( [.downloading, .processing, .finished]
)。 可以选择wait(for:timeout: enforceOrder: )
更好吗?
而不是一次满足3次期望,而是使用其中的三个-每次声明一个状态值。 通过将enforceOrder
设置为true
我们确保以正确的顺序满足所有要求。
两种方法都可以完成工作,但是使用后者可以更轻松地测试订单。 我们也不需要其他可能会降低可读性的变量。 选择更适合您的方式。
最后但并非最不重要的一点: expectationDescription
确实很重要! 它旨在帮助诊断故障。 您提供的描述越好,就越容易理解并维护测试。
我看到了许多关于ExpectationDescription的奇怪约定,所有这些都巧妙地使我相信某些开发人员的惰性很聪明smart。 复制测试方法名称或使用#function
宏都不会帮助读者掌握您的代码或理解故障报告。 如果很难给出一个好的描述,也许您正在测试错误的东西?
完美的描述告诉读者代码正在等待什么:
期望(描述:“代表被两次调用”) 期望(描述:“安全存储已解锁”) 期望(描述:“用户已登录”)
它给出了清晰整洁的故障报告:
超过0.5秒的超时,未实现期望: “用户已登录” 。
通过在BlaBlaCar上应用这些技巧,我们可以编写更短,更整洁的单元测试。 我们涵盖了更多的用例和边缘方案,同时简化了可维护性。 这是一个很好的例子,说明如何稍微改进工具集可以帮助提高生产率。
要进一步阅读本主题,我建议查看XCTestExpectation和XCTestCase的文档页面。 不要犹豫,与我分享您的发现。
想直接联系我吗? 我是 在Twitter上创建的 。