使用 PyHamcrest 执行强大的单元测试

使用此框架编写断言,提高开发测试的准确性。
236 位读者喜欢这个。

单元测试位于测试金字塔的底部。单元测试一次测试一个代码单元——通常是一个函数或方法。

通常,单个单元测试旨在测试通过函数的特定流程或特定的分支选择。这使得可以轻松地将失败的单元测试和导致其失败的错误进行映射。

理想情况下,单元测试使用很少或不使用外部资源,从而隔离它们并使其更快。

单元测试套件通过在开发过程的早期发出问题信号来帮助维护高质量的产品。有效的单元测试会在代码离开开发人员机器之前,或者至少在专用分支上的持续集成环境中捕获错误。这标志着好的和坏的单元测试之间的区别:好的测试通过及早发现错误并加快测试速度来提高开发人员的生产力。坏的测试会降低开发人员的生产力。

当测试偶然功能时,生产力通常会下降。即使代码仍然正确,测试也会在代码更改时失败。发生这种情况是因为输出不同,但方式不是函数合约的一部分。

因此,好的单元测试是帮助执行函数承诺的合约的测试。

如果单元测试中断,则合约被违反,应明确修改(通过更改文档和测试),或修复(通过修复代码并保持测试不变)。

虽然将测试限制为仅执行公共合约是一项复杂的技能,但有一些工具可以提供帮助。

其中一个工具是 Hamcrest,这是一个用于编写断言的框架。Hamcrest 框架最初是为基于 Java 的单元测试而发明的,如今它支持多种语言,包括 Python

Hamcrest 旨在使测试断言更易于编写且更精确。

def add(a, b):
    return a + b

from hamcrest import assert_that, equal_to

def test_add():
    assert_that(add(2, 2), equal_to(4))  

这是一个简单的断言,用于简单的功能。如果我们想断言更复杂的东西怎么办?

def test_set_removal():
    my_set = {1, 2, 3, 4}
    my_set.remove(3)
    assert_that(my_set, contains_inanyorder([1, 2, 4]))
    assert_that(my_set, is_not(has_item(3)))

请注意,我们可以简洁地断言结果具有 124,顺序不限,因为集合不保证顺序。

我们还可以使用 is_not 轻松否定断言。这有助于我们编写精确的断言,从而使我们能够将自己限制为执行函数的公共合约。

但是,有时,没有内置功能精确地满足我们的需求。在这些情况下,Hamcrest 允许我们编写自己的匹配器。

想象一下以下函数

def scale_one(a, b):
    scale = random.randint(0, 5)
    pick = random.choice([a,b])
    return scale * pick

我们可以自信地断言,结果可以均匀地除以至少一个输入。

匹配器继承自 hamcrest.core.base_matcher.BaseMatcher,并覆盖两个方法

class DivisibleBy(hamcrest.core.base_matcher.BaseMatcher):

    def __init__(self, factor):
        self.factor = factor

    def _matches(self, item):
        return (item % self.factor) == 0

    def describe_to(self, description):
        description.append_text('number divisible by')
        description.append_text(repr(self.factor))

编写高质量的 describe_to 方法非常重要,因为这是测试失败时将显示的消息的一部分。

def divisible_by(num):
    return DivisibleBy(num)

按照惯例,我们将匹配器包装在一个函数中。有时这使我们有机会进一步处理输入,但在这种情况下,无需进一步处理。

def test_scale():
    result = scale_one(3, 7)
    assert_that(result,
                any_of(divisible_by(3),
                       divisible_by(7)))

请注意,我们将 divisible_by 匹配器与内置的 any_of 匹配器结合使用,以确保我们仅测试合约承诺的内容。

在编辑本文时,我听到一个传言,说“Hamcrest”这个名字被选为“matches”的字谜。嗯...

>>> assert_that("matches", contains_inanyorder(*"hamcrest")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/moshez/src/devops-python/build/devops/lib/python3.6/site-packages/hamcrest/core/assert_that.py", line 43, in assert_that
    _assert_match(actual=arg1, matcher=arg2, reason=arg3)
  File "/home/moshez/src/devops-python/build/devops/lib/python3.6/site-packages/hamcrest/core/assert_that.py", line 57, in _assert_match
    raise AssertionError(description) 
AssertionError:
Expected: a sequence over ['h', 'a', 'm', 'c', 'r', 'e', 's', 't'] in any order
      but: no item matches: 'r' in ['m', 'a', 't', 'c', 'h', 'e', 's']

经过更多研究,我找到了谣言的来源:它是“matchers”的字谜。

>>> assert_that("matchers", contains_inanyorder(*"hamcrest"))
>>> 

如果您还没有为您的 Python 代码编写单元测试,那么现在是开始的好时机。如果您正在为您的 Python 代码编写单元测试,使用 Hamcrest 将使您的断言精确——既不多也不少于您想要测试的内容。这将减少修改代码时的误报,并减少花费在修改工作代码的测试上的时间。

标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
自 1998 年以来,Moshe 一直参与 Linux 社区,帮助举办 Linux“安装派对”。自 1999 年以来,他一直在编写 Python 代码,并为核心 Python 解释器做出了贡献。Moshe 在这些术语存在之前就一直是 DevOps/SRE,他非常关心软件可靠性、构建可重复性以及其他此类事情。

评论已关闭。

Creative Commons License本作品根据 知识共享许可协议 授权,许可协议为 Creative Commons Attribution-Share Alike 4.0 International License。
© . All rights reserved.