关于python:Python函数式编程系列008可测

4次阅读

共计 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")

很显然,这个单元测试一眼就看出了几个问题:

  1. 事实上,咱们只是从新写了一遍原有的代码,并没有真的测试。
  2. 即便咱们抵赖这种写法,也有肯定概率在靠近凌晨的时候(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)

当然,这个解决方案是针对 工夫 这个事的,咱们遇到的副作用可能不止这一种,可能是读取配置、数据库交互等等,这种计划无奈解决这些事。

另一类就是测试畛域的概念,比方 fakemockstub 之类的概念了,咱们在上面的工作中当然也会用到 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'

咱们发现这么写有诸多益处了:

  1. 整个函数变成了无副作用了,副作用被隔离在了参数外面
  2. 因为无副作用了,咱们只须要本人制作相应的「假」函数就能够模仿要的输出了,特地是针对 Void -> A 这种类型的函数。
  3. 咱们能够通过假函数的形式模仿任何一个状态时的操作,这使得咱们下面说的调度逻辑能够变得能够测试了。
  4. 咱们在具体调用的时候,因为设定了参数的默认值,因而其具体应用的办法并没发生变化。

这种把副作用写在参数的办法,咱们将在之后遇到相似的计划(无副作用的随机数),以及在后续的文章中看到 Monad 如何解决此类问题。

不过,这篇文章引申出了「可测性」的概念,一般来说,没有副作用的函数是相对可测的,并且能够在单元测试阶段实现测试的。带有副作用的函数 / 办法会使得测试变得艰难。因而,通过单元测试及覆盖率的概念,咱们能够将大多数问题裸露在上线前,这是十分 Fancy 的一种形式。如果加上类型推导,这种零碎的可用性的判断将会更加完满(当然,这是 Python 这种语言很难做到的,不过能够基于 mypy 做相似的事)。

当然,这也是函数式编程测试的开始,咱们前面将会介绍另一个独有的函数式编程的测试概念——基于性质的测试(Property-based testing),而后介绍基于它受启发的一些不错的第三方模块和办法。

正文完
 0