Swift中与日期相关的当前代码的确定性单元测试
自从我在Envelope框架上发表我的上一篇文章以来已经有一段时间了-Alamofire周围的薄包装使编写网络代码的单元测试变得轻而易举。 可能一开始这篇文章太大了,但是无论如何我都希望听到更多的反馈。今天,我将分享我几年前一直在使用的一个非常简单的技巧,该技巧简化了另一个方面编写单元测试的方法:测试使用当前日期/时间的代码。
问题
因此,想象一下您正在编写一种方法,如果记录已过期,则请求更新记录:
在这里, entityManager
是一个对象,负责通过id
请求和存储对记录的更新。
如何对这种功能进行单元测试?
看到问题了吗? 我们需要一个nonExpiredRecord
,即条件为record.lastUpdateTime <= dateOfExpiration
为false
的Record
实例。 因此, RecordFixture.nonExpiredRecord()
应该生成一个带有lastUpdateTime
更新为当前时间的记录! 想象一下,当Record
是一个struct
时,会是什么样子–您将必须复制该结构的所有字段,并用当前日期更新一个字段。 甚至更糟的是,当这样的设备来自例如保存的网络响应时,其模式可能会随着时间而变化,支持这样的测试代码变得很痛苦,并且是CI失败的根源。 即使夹具的结构正确,但如果经过了实际时间,在调试器中单步执行功能也可能导致将条件评估为错误的结果。
解决方案
冻结时间。
/明显的模式开启/单元测试应该是确定性的。
甚至那些处理当前时间的事件。 /关闭明显的模式/
想象一下,在单元测试套件下运行时调用Date()
总是会返回,例如, 1 January 2016 12:00GMT
? 然后为总是“未过期”的记录创建测试夹具将是微不足道的,不是吗?
那么,如何在单元测试中覆盖Date()
,使其返回预定义的日期呢? 不管是好是坏,这是不可能直接实现的—复杂的方法实现现在已经成为过去。
我们可以做的是:
- 提供
Date()
的替代方法,该替代方法将在正常程序执行下返回当前日期,并具有单元测试套件覆盖其行为的能力; - 在测试套件中,将其覆盖以始终返回预定义的日期;
- 禁止使用lint规则或git pre-commit钩子,或同时使用这两种方法在源代码中使用
Date()
初始值设定项。
让我们一步一步地做。
1.提供获取当前日期的替代方法。
2.覆盖测试套件下的当前日期行为。
如果未使用Quick
,则可以通过重写单元测试套件类的class func load()
函数来完成相同的操作。
因此,现在,在测试套件中运行时,主应用程序模块中调用Date.current
的代码将始终返回Date.mockDate
的值,因此测试治具现在可以是常量(例如,从保存的JSON加载),也可以构造使用已知的模拟日期:
3.禁止在代码中使用Date()
。
让我们在git pre-commit钩子脚本中添加一个部分,并使用简单的regexp查找所有出现在舞台区域的Date()
模式的出现:
骇客入侵!