单元测试位于测试金字塔的底部。单元测试一次测试一个代码单元——通常是一个函数或方法。
通常,单个单元测试旨在测试通过函数的特定流程或特定的分支选择。这使得可以轻松地将失败的单元测试和导致其失败的错误进行映射。
理想情况下,单元测试使用很少或不使用外部资源,从而隔离它们并使其更快。
单元测试套件通过在开发过程的早期发出问题信号来帮助维护高质量的产品。有效的单元测试会在代码离开开发人员机器之前,或者至少在专用分支上的持续集成环境中捕获错误。这标志着好的和坏的单元测试之间的区别:好的测试通过及早发现错误并加快测试速度来提高开发人员的生产力。坏的测试会降低开发人员的生产力。
当测试偶然功能时,生产力通常会下降。即使代码仍然正确,测试也会在代码更改时失败。发生这种情况是因为输出不同,但方式不是函数合约的一部分。
因此,好的单元测试是帮助执行函数承诺的合约的测试。
如果单元测试中断,则合约被违反,应明确修改(通过更改文档和测试),或修复(通过修复代码并保持测试不变)。
虽然将测试限制为仅执行公共合约是一项复杂的技能,但有一些工具可以提供帮助。
其中一个工具是 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)))
请注意,我们可以简洁地断言结果具有 1
、2
和 4
,顺序不限,因为集合不保证顺序。
我们还可以使用 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 将使您的断言精确——既不多也不少于您想要测试的内容。这将减少修改代码时的误报,并减少花费在修改工作代码的测试上的时间。
评论已关闭。