Swift中与日期相关的当前代码的确定性单元测试

自从我在Envelope框架上发表我的上一篇文章以来已经有一段时间了-Alamofire周围的薄包装使编写网络代码的单元测试变得轻而易举。 可能一开始这篇文章太大了,但是无论如何我都希望听到更多的反馈。今天,我将分享我几年前一直在使用的一个非常简单的技巧,该技巧简化了另一个方面编写单元测试的方法:测试使用当前日期/时间的代码。

问题

因此,想象一下您正在编写一种方法,如果记录已过期,则请求更新记录:

在这里, entityManager是一个对象,负责通过id请求和存储对记录的更新。

如何对这种功能进行单元测试?

看到问题了吗? 我们需要一个nonExpiredRecord ,即条件为record.lastUpdateTime <= dateOfExpirationfalseRecord实例。 因此, RecordFixture.nonExpiredRecord()应该生成一个带有lastUpdateTime更新为当前时间的记录! 想象一下,当Record是一个struct时,会是什么样子–您将必须复制该结构的所有字段,并用当前日期更新一个字段。 甚至更糟的是,当这样的设备来自例如保存的网络响应时,其模式可能会随着时间而变化,支持这样的测试代码变得很痛苦,并且是CI失败的根源。 即使夹具的结构正确,但如果经过了实际时间,在调试器中单步执行功能也可能导致将条件评估为错误的结果。

解决方案

冻结时间。

/明显的模式开启/单元测试应该是确定性的。
甚至那些处理当前时间的事件。 /关闭明显的模式/

想象一下,在单元测试套件下运行时调用Date()总是会返回,例如, 1 January 2016 12:00GMT ? 然后为总是“未过期”的记录创建测试夹具将是微不足道的,不是吗?

那么,如何在单元测试中覆盖Date() ,使其返回预定义的日期呢? 不管是好是坏,这是不可能直接实现的—复杂的方法实现现在已经成为过去。

我们可以做的是:

  1. 提供Date()的替代方法,该替代方法将在正常程序执行下返回当前日期,并具有单元测试套件覆盖其行为的能力;
  2. 在测试套件中,将其覆盖以始终返回预定义的日期;
  3. 禁止使用lint规则或git pre-commit钩子,或同时使用这两种方法在源代码中使用Date()初始值设定项。

让我们一步一步地做。

1.提供获取当前日期的替代方法。

2.覆盖测试套件下的当前日期行为。

如果未使用Quick ,则可以通过重写单元测试套件类的class func load()函数来完成相同的操作。

因此,现在,在测试套件中运行时,主应用程序模块中调用Date.current的代码将始终返回Date.mockDate的值,因此测试治具现在可以是常量(例如,从保存的JSON加载),也可以构造使用已知的模拟日期:

3.禁止在代码中使用Date()

让我们在git pre-commit钩子脚本中添加一个部分,并使用简单的regexp查找所有出现在舞台区域的Date()模式的出现:

骇客入侵!