共计 2192 个字符,预计需要花费 6 分钟才能阅读完成。
咱们在之前的文章之中,曾经重复地强调了很多函数式编程的长处,例如表达能力,提早计算的益处之类的。但其实一个更大的有点其实是可测性。本篇文章也是传播整个系列要表白的外围,咱们不是要齐全排除过程式、副作用等概念,而是无限的应用,并且能在现有代码的根底上做改进。
缘起
上面,咱们看一个例子:一个公司心愿设计一个基于工夫的调度器,它们能够提供一个比 crontab
更欠缺的语法,比方能够基于每个月前三天、每周周末、每个月第二周的第一天之类这些表述。设计这个调度器时候,就会波及到很多无关工夫的函数,比方,上面是一个可能要实现的函数:
from datetime import datetime, timedelta
def yesterday_str() -> str:
"""获取昨日的工夫的字符串(YYYYMMDD)"""
return (datetime.now() - timedelta(days=1)
).strftime("%Y%m%d")
这是一个最直观的实现,然而,这个函数咱们发现是不可测的。起因大家应该看进去,就是因为 datetime.now()
是带有副作用的。具体咱们能够把测试中可能遇到的问题例举如下:
单元测试例子中的问题
咱们会如何写这个函数的单元测试呢,很显然,大部分人会这么写:
def test_yesterday_str():
assert yesterday_str() == (datetime.now() - timedelta(days=1)
).strftime("%Y%m%d")
很显然,这个单元测试一眼就看出了几个问题:
- 事实上,咱们只是从新写了一遍原有的代码,并没有真的测试。
- 即便咱们抵赖这种写法,也有肯定概率在靠近凌晨的时候(23:59:59 秒时),这个测试不通过,但这又不是因为性能实现的问题导致的谬误。
整合测试中的问题
在理论测试中,可能某些整合的局部更难测试到,比方咱们上面一个调用下面函数的函数,它的性能是在每个月 1 号执行一个工作:
def run_at_first_day():
if yesterday_str()[-2:] == '01':
do_something()
这个例子不仅把副作用又一步步传递上来了,而且在测试中,咱们如果不是在 1 号进行测试,咱们就只能测到 do_something
的逻辑而测不到 run_at_first_day
这个调度的逻辑。而能够设想,在这个零碎内,这种例子会十分多。
如何解决
惯例解决方案
惯例的解决方案,第一个就是批改零碎工夫。Python
中有一个 FreezeGun
的模块,就是做相似事的:
from freezegun import freeze_time
import datetime
@freeze_time("2012-01-14")
def test():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
当然,这个解决方案是针对 工夫 这个事的,咱们遇到的副作用可能不止这一种,可能是读取配置、数据库交互等等,这种计划无奈解决这些事。
另一类就是测试畛域的概念,比方 fake
、mock
、stub
之类的概念了,咱们在上面的工作中当然也会用到 fake
的概念,然而不须要纠结于这些简单的概念。
把副作用函数作为参数
咱们改写成上面的形式,就发现整个函数变得可测了:
def yesterday_str(now_func = date.now) -> str:
assert yesterday_str() == (now_func() - timedelta(days=1)
).strftime("%Y%m%d")
具体的测试写法如下:
def fake_now(now_str):
def helper():
return datetime.strptime(now_str, "%Y-%m-%d")
return helper
def test_yesterday_str():
return yesterday_str(fake_now('2020-01-01')) == '2019-12-31'
咱们发现这么写有诸多益处了:
- 整个函数变成了无副作用了,副作用被隔离在了参数外面
- 因为无副作用了,咱们只须要本人制作相应的「假」函数就能够模仿要的输出了,特地是针对
Void -> A
这种类型的函数。 - 咱们能够通过假函数的形式模仿任何一个状态时的操作,这使得咱们下面说的调度逻辑能够变得能够测试了。
- 咱们在具体调用的时候,因为设定了参数的默认值,因而其具体应用的办法并没发生变化。
这种把副作用写在参数的办法,咱们将在之后遇到相似的计划(无副作用的随机数),以及在后续的文章中看到 Monad 如何解决此类问题。
不过,这篇文章引申出了「可测性」的概念,一般来说,没有副作用的函数是相对可测的,并且能够在单元测试阶段实现测试的。带有副作用的函数 / 办法会使得测试变得艰难。因而,通过单元测试及覆盖率的概念,咱们能够将大多数问题裸露在上线前,这是十分 Fancy 的一种形式。如果加上类型推导,这种零碎的可用性的判断将会更加完满(当然,这是 Python 这种语言很难做到的,不过能够基于 mypy
做相似的事)。
当然,这也是函数式编程测试的开始,咱们前面将会介绍另一个独有的函数式编程的测试概念——基于性质的测试(Property-based testing),而后介绍基于它受启发的一些不错的第三方模块和办法。