使用 PyHamcrest 執行健壯的單元測試

Moshe Zadka發表於2019-01-29

使用此框架編寫斷言,提高開發測試的準確性。

測試金字塔的底部是單元測試。單元測試每次只測試一個程式碼單元,通常是一個函式或方法。

通常,設計單個單元測試是為了測試通過一個函式或特定分支的特定執行流程,這使得將失敗的單元測試和導致失敗的 bug 對應起來變得容易。

理想情況下,單元測試很少使用或不使用外部資源,從而隔離它們並使它們更快。

單元測試套件通過在開發過程的早期發現問題來幫助維護高質量的產品。有效的單元測試可以在程式碼離開開發人員機器之前捕獲 bug,或者至少可以在特定分支上的持續整合環境中捕獲 bug。這標誌著好的和壞的單元測試之間的區別:好的測試通過儘早捕獲 bug 並使測試更快來提高開發人員的生產力。壞的測試降低了開發人員的工作效率。

當測試附帶的特性時,生產率通常會降低。當程式碼更改時測試會失敗,即使它仍然是正確的。發生這種情況是因為輸出的不同,但在某種程度上是因為它不是函式契約function’s contract的一部分。

因此,一個好的單元測試可以幫助執行函式所提交的契約。

如果單元測試中斷,那意味著該契約被違反了,應該(通過更改文件和測試)明確修改,或者(通過修復程式碼並保持測試不變)來修復。

雖然將測試限制為只執行公共契約是一項需要學習的複雜技能,但有一些工具可以提供幫助。

其中一個工具是 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 允許我們編寫自己的匹配器matchers

想象一下以下功能:

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 將允許你使你的斷言更加精確,既不會比你想要測試的多也不會少。這將在修改程式碼時減少誤報,並減少修改工作程式碼的測試所花費的時間。


via: https://opensource.com/article/18/8/robust-unit-tests-hamcrest

作者:Moshe Zadka 選題:lujun9972 譯者:MjSeven 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章