<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          5 分鐘快速上手 pytest 測試框架

          共 11649字,需瀏覽 24分鐘

           ·

          2021-04-20 05:28

          本文將會(huì)把關(guān)于 Pytest 的內(nèi)容分上下兩篇,上篇主要涉及關(guān)于 pytest 概念以及功能組件知識(shí)的介紹,下篇主要以一個(gè) Web 項(xiàng)目來將 Pytest 運(yùn)用實(shí)踐中。

          為什么要做單元測試

          相信很多 Python 使用者都會(huì)有這么一個(gè)經(jīng)歷,為了測試某個(gè)模塊或者某個(gè)函數(shù)是否輸出自己預(yù)期的結(jié)果,往往會(huì)對(duì)產(chǎn)出結(jié)果的部分使用 print() 函數(shù)將其打印輸出到控制臺(tái)上。
          def myfunc(*args, **kwargs):
              do_something()
              data = ...
              print(data)
          在一次次改進(jìn)過程中會(huì)不得不經(jīng)常性地使用 print() 函數(shù)來確保結(jié)果的準(zhǔn)確性,但同時(shí),也由于要測試的模塊或者函數(shù)變多,代碼中也會(huì)逐漸遺留著各種未被去掉或注釋的 print() 調(diào)用,讓整個(gè)代碼變得不是那么簡潔得體。
          在編程中往往會(huì)存在「單元測試」這么一個(gè)概念,即指對(duì)軟件中的最小可測試單元進(jìn)行檢查和驗(yàn)證。這個(gè)最小可測單元可以是我們的表達(dá)式、函數(shù)、類、模塊、包中的任意一種或組合,因此我們可以將使用 print() 進(jìn)行測試的步驟統(tǒng)一地放到單元測試中來進(jìn)行。
          在 Python 中官方早已經(jīng)為我們內(nèi)置好了用以進(jìn)行單元測試的模塊 unittest。但對(duì)于新手來說,unittest 在學(xué)習(xí)曲線上是稍微有點(diǎn)難度的,因?yàn)槭切枰ㄟ^繼承測試用例類(TestCase)來進(jìn)行封裝,所以需要對(duì)面向?qū)ο蟮闹R(shí)有足夠多的了解;而和類綁定在一起就意味著如果想要實(shí)現(xiàn)定制化或者模塊解耦,可能就需要多花一些時(shí)間在設(shè)計(jì)劃分上。

          所以,為了能讓測試變得簡單且具備可擴(kuò)展性,一個(gè)名為 pytest 的測試框架在 Python 社區(qū)中誕生了,使用 pytest 我們可以不用考慮如何基于 TestCase 來實(shí)現(xiàn)我們的測試,我們只需要簡單到保持我們?cè)械拇a邏輯不變,外加一個(gè) assert 關(guān)鍵字來斷言結(jié)果,剩下的部分 pytest 會(huì)幫我們處理。
          # main.py

          import pytest

          raw_data = read_data(...)

          def test_myfunc(*args, **kwargs):
              do_something()
              data = ...
              assert data == raw_data

          if __name__ == '__main__':
              pytest.main()
          之后我們只需要運(yùn)行包含上述代碼的 main.py 文件,就能在終端控制臺(tái)上看到 pytest 為我們測試得到的結(jié)果。如果結(jié)果通過,則不會(huì)有過多的信息顯示,如果測試失敗,則會(huì)拋出錯(cuò)誤信息并告知運(yùn)行時(shí) data 里的內(nèi)容是什么。
          盡管說 pytest 已經(jīng)足夠簡單,但它也提供了許多實(shí)用的功能(如:依賴注入),這些功能本身是存在著一些概念層面的知識(shí);但這并不意味著勸退想要使用 pytest 來測試自己代碼的人,而是讓我們擁有更多的選擇,因此只有對(duì) pytest 的這些功能及其概念有了更好地了解,我們才能夠充分發(fā)揮 pytest 的威力。

          快速實(shí)現(xiàn)你的第一個(gè) Pytest 測試

          通過 pip install pytest 安裝 pytest 之后,我們就可以快速實(shí)現(xiàn)我們的第一個(gè)測試。
          首先我們可以任意新建一個(gè) Python 文件,這里我直接以 test_main.py 命名,然后當(dāng)中留存如下內(nèi)容:
          from typing import Union

          import pytest

          def add(
              x: Union[int, float], 
              y: Union[int, float],
          )
           -> Union[int, float]:

              return x + y

          @pytest.mark.parametrize(
              argnames="x,y,result"
              argvalues=[
                  (1,1,2),
                  (2,4,6),
                  (3.3,3,6.3),
              ]
          )
          def test_add(
              x: Union[int, float], 
              y: Union[int, float],
              result: Union[int, float],
          )
          :

              assert add(x, y) == result
          之后將終端切換到該文件所處路徑下,然后運(yùn)行 pytest -v,就會(huì)看到 pytest 已經(jīng)幫我們將待測試的參數(shù)傳入到測試函數(shù)中,并實(shí)現(xiàn)對(duì)應(yīng)的結(jié)果:

          可以看到我們無需重復(fù)地用 for 循環(huán)傳參,并且還能直觀地從結(jié)果中看到每次測試中傳入?yún)?shù)的具體數(shù)值是怎樣。這里我們只通過 pytest 提供的 mark.parametrize 裝飾器就搞定了。也說明 pytest 的上手程度是比較容易的,只不過我們需要稍微了解一下這個(gè)框架中的一些概念。

          Pytest 概念與用法

          命名

          如果需要 pytest 對(duì)你的代碼進(jìn)行測試,首先我們需要將待測試的函數(shù)、類、方法、模塊甚至是代碼文件,默認(rèn)都是以 test_* 開頭或是以 *_test 結(jié)尾,這是為了遵守標(biāo)準(zhǔn)的測試約定。如果我們將前面快速上手的例子文件名中的 test_ 去掉,就會(huì)發(fā)現(xiàn) pytest 沒有收集到對(duì)應(yīng)的測試用例。
          當(dāng)然我們也可以在 pytest 的配置文件中修改不同的前綴或后綴名,就像官方給出的示例這樣:
          # content of pytest.ini
          # Example 1: have pytest look for "check" instead of "test"
          [pytest]
          python_files = check_*.py
          python_classes = Check
          python_functions = *_check
          但通常情況下我們使用默認(rèn)的 test 前后綴即可。如果我們只想挑選特定的測試用例或者只對(duì)特定模塊下的模塊進(jìn)測試,那么我們可以在命令行中通過雙冒號(hào)的形式進(jìn)行指定,就像這樣:
          pytest test.py::test_demo
          pytest test.py::TestDemo::test_demo

          標(biāo)記(mark)

          在 pytest 中,mark 標(biāo)記是一個(gè)十分好用的功能,通過標(biāo)記的裝飾器來裝飾我們的待測試對(duì)象,讓 pytest 在測試時(shí)會(huì)根據(jù) mark 的功能對(duì)我們的函數(shù)進(jìn)行相應(yīng)的操作。
          官方本身提供了一些預(yù)置的 mark 功能,我們只挑常用的說。

          參數(shù)測試:pytest.parametrize

          正如前面的示例以及它的命名意思一樣,mark.parametrize 主要就是用于我們想傳遞不同參數(shù)或不同組合的參數(shù)到一個(gè)待測試對(duì)象上的這種場景。
          正如我們前面的 test_add() 示例一樣,分別測試了:
          • 當(dāng) x=1y=1 時(shí),結(jié)果是否為 result=2 的情況
          • 當(dāng) x=2y=4 時(shí),結(jié)果是否為 result=6 的情況
          • 當(dāng) x=3.3y=3 時(shí),結(jié)果是否為 result=6.3 的情況
          • ……
          我們也可以將參數(shù)堆疊起來進(jìn)行組合,但效果也是類似:
          import pytest

          @pytest.mark.parametrize("x", [0, 1])
          @pytest.mark.parametrize("y", [2, 3])
          @pytest.mark.parametrize("result", [2, 4])
          def test_add(x, y, result):
              assert add(x,y) == result
          當(dāng)然如果我們有足夠多的參數(shù),只要寫進(jìn)了 parametrize 中,pytest 依舊能幫我們把所有情況都給測試一遍。這樣我們就再也不用寫多余的代碼。
          但需要注意的是,parametrize 和我們后面將要講到的一個(gè)重要的概念 fixture 會(huì)有一些差異:前者主要是模擬不同參數(shù)下時(shí)待測對(duì)象會(huì)輸出怎樣的結(jié)果,而后者是在固定參數(shù)或數(shù)據(jù)的情況下,去測試會(huì)得到怎樣的結(jié)果。

          跳過測試

          有些情況下我們的代碼包含了針對(duì)不同情況、版本或兼容性的部分,那么這些代碼通常只有在符合了特定條件下可能才適用,否則執(zhí)行就會(huì)有問題,但產(chǎn)生的這個(gè)問題的原因不在于代碼邏輯,而是因?yàn)橄到y(tǒng)或版本信息所導(dǎo)致,那如果此時(shí)作為用例測試或測試失敗顯然不合理。比如我針對(duì) Python 3.3 版本寫了一個(gè)兼容性的函數(shù),add(),但當(dāng)版本大于 Python 3.3 時(shí)使用必然會(huì)出現(xiàn)問題。
          因此為了適應(yīng)這種情況 pytest 就提供了 mark.skipmark.skipif 兩個(gè)標(biāo)記,當(dāng)然后者用的更多一些。
          import pytest
          import sys

          @pytest.mark.skipif(sys.version_info >= (3,3))
          def test_add(x, y, result):
              assert add(x,y) == result
          所以當(dāng)我們加上這一標(biāo)記之后,每次在測試用例之前使用 sys 模塊判斷 Python 解釋器的版本是否大于 3.3,大于則會(huì)自動(dòng)跳過。

          預(yù)期異常

          代碼只要是人寫的必然會(huì)存在不可避免的 BUG,當(dāng)然有一些 BUG 我們作為寫代碼的人是可以預(yù)期得到的,這類特殊的 BUG 通常也叫異常(Exception)。比如我們有一個(gè)除法函數(shù):
          def div(x, y):
              return x / y
          但根據(jù)我們的運(yùn)算法則可以知道,除數(shù)不能為 0;因此如果我們傳遞 y=0 時(shí),必然會(huì)引發(fā) ZeroDivisionError 異常。所以通常的做法要么就用 try...exception 來捕獲異常,并且拋出對(duì)應(yīng)的報(bào)錯(cuò)信息(我們也可以使用 if 語句進(jìn)行條件判斷,最后也同樣是拋出報(bào)錯(cuò)):
          def div(x, y):
              try:
                  return x/y
              except ZeroDivisionError:
                  raise ValueError("y 不能為 0")
          因此,此時(shí)在測試過程中,如果我們想測試異常斷言是否能被正確拋出,此時(shí)就可以使用 pytest 提供的 raises() 方法:
          import pytest

          @pytest.mark.parametrize("x", [1])
          @pytest.mark.parametrize("y", [0])
          def test_div(x, y):
              with pytest.raises(ValueError):
                  div(x, y)
          這里需要注意,我們需要斷言捕獲的是引發(fā) ZeroDivisionError 后我們自己指定拋出的 ValueError,而非前者。當(dāng)然我們可以使用另外一個(gè)標(biāo)記化的方法(pytest.mark.xfail)來和 pytest.mark.parametrize 相結(jié)合:

          @pytest.mark.parametrize(
              "x,y,result"
              [
                  pytest.param(1,0None, marks=pytest.mark.xfail(raises=(ValueError))),
              ]
          )
          def test_div_with_xfail(x, y, result):
              assert div(x,y) == result
          這樣測試過程中會(huì)直接標(biāo)記出失敗的部分。

          Fixture

          在 pytest 的眾多特性中,最令人感到驚艷的就是 fixture。關(guān)于 fixture 的翻譯大部分人都直接將其直譯為了「夾具」一詞,但如果你有了解過 Java Spring 框架的 那么你在實(shí)際使用中你就會(huì)更容易將其理解為 IoC 容器類似的東西,但我自己認(rèn)為它叫「載具」或許更合適。
          因?yàn)橥ǔG闆r下都是 fixture 的作用往往就是為我們的測試用例提供一個(gè)固定的、可被自由拆裝的通用對(duì)象,本身就像容器一樣承載了一些東西在里面;讓我們使用它進(jìn)行我們的單元測試時(shí),pytest 會(huì)自動(dòng)向載具中注入對(duì)應(yīng)的對(duì)象。
          這里我稍微模擬了一下我們?cè)谑褂檬褂脭?shù)據(jù)庫時(shí)的情況。通常我們會(huì)通過一個(gè)數(shù)據(jù)庫類創(chuàng)建一下數(shù)據(jù)庫對(duì)象,然后使用前先進(jìn)行連接 connect(),接著進(jìn)行操作,最后使用完之后斷開連接 close() 以釋放資源。
          # test_fixture.py

          import pytest

          class Database(object):

              def __init__(self, database):
                  self.database = database
              
              def connect(self):
                  print(f"\n{self.database} database has been connected\n")

              def close(self):
                  print(f"\n{self.database} database has been closed\n")

              def add(self, data):
                  print(f"`{data}` has been add to database.")
                  return True

          @pytest.fixture
          def myclient():
              db = Database("mysql")
              db.connect()
              yield db
              db.close()

          def test_foo(myclient):
              assert myclient.add(1) == True
          在這段代碼中,實(shí)現(xiàn)載具的關(guān)鍵是 @pytest.fixture 這一行裝飾器代碼,通過該裝飾器我們可以直接使用一個(gè)帶有資源的函數(shù)將其作為我們的載具,在使用時(shí)將函數(shù)的簽名(即命名)作為參數(shù)傳入到我們的測試用例中,在運(yùn)行測試時(shí) pytest 則會(huì)自動(dòng)幫助我們進(jìn)行注入。

          在注入的過程中 pytest 會(huì)幫我們執(zhí)行 myclient()db 對(duì)象的 connect() 方法調(diào)用模擬數(shù)據(jù)庫連接的方法,在測試完成之后會(huì)再次幫我們調(diào)用 close() 方法釋放資源。
          pytest 的 fixture 機(jī)制是一個(gè)讓我們能實(shí)現(xiàn)復(fù)雜測試的關(guān)鍵,試想我們以后只需要寫好一個(gè)帶有測試數(shù)據(jù)的 fixture,就可以在不同的模塊、函數(shù)或者方法中多次使用,真正做到「一次生成,處處使用」。
          當(dāng)然 pytest 給我們提供了可調(diào)節(jié)載具作用域(scope)的情況,從小到大依次是:
          • function:函數(shù)作用域(默認(rèn))
          • class:類作用域
          • module:模塊作用域
          • package:包作用域
          • session:會(huì)話作用域
          載具會(huì)隨著作用域的生命周期而誕生、銷毀。所以如果我們希望創(chuàng)建的載具作用域范圍增加,就可以在 @pytest.fixture() 中多增加一個(gè) scope 參數(shù),從而提升載具作用的范圍。
          雖然 pytest 官方為我們提供了一些內(nèi)置的通用載具,但通常情況下我們自己自定義的載具會(huì)更多一些。所以我們都可以將其放到一個(gè)名為 conftest.py 文件中進(jìn)行統(tǒng)一管理:
          # conftest.py

          import pytest

          class Database:
              def __init__(self, database):
                  self.database:str = database
              
              def connect(self):
                  print(f"\n{self.database} database has been connected\n")

              def close(self):
                  print(f"\n{self.database} database has been closed\n")

              def add(self, data):
                  print(f"\n`{data}` has been add to database.")
                  return True

          @pytest.fixture(scope="package")
          def myclient():
              db = Database("mysql")
              db.connect()
              yield db
              db.close()
          因?yàn)槲覀兟暶髁俗饔糜驗(yàn)橥粋€(gè)包,那么在同一個(gè)包下我們?cè)賹⑶懊娴?test_add() 測試部分稍微修改一下,無需顯式導(dǎo)入 myclient 載具就可以直接注入并使用:
          from typing import Union

          import pytest

          def add(
              x: Union[int, float], 
              y: Union[int, float],
          )
           -> Union[int, float]:

              return x + y

          @pytest.mark.parametrize(
              argnames="x,y,result"
              argvalues=[
                  (1,1,2),
                  (2,4,6),
              ]
          )
          def test_add(
              x: Union[int, float], 
              y: Union[int, float],
              result: Union[int, float],
              myclient
          )
          :

              assert myclient.add(x) == True
              assert add(x, y) == result
          之后運(yùn)行 pytest -vs 即可看到輸出的結(jié)果:

          Pytest 擴(kuò)展

          對(duì)于每個(gè)使用框架的人都知道,框架生態(tài)的好壞會(huì)間接影響框架的發(fā)展(比如 Django 和 Flask)。而 pytest 預(yù)留了足夠多的擴(kuò)展空間,加之許多易用的特性,也讓使用 pytest 存在了眾多插件或第三方擴(kuò)展的可能。
          根據(jù)官方插件列表所統(tǒng)計(jì),目前 pytest 有多大 850 個(gè)左右的插件或第三方擴(kuò)展,我們可以在 pytest 官方的 Reference 中找到 Plugin List 這一頁面查看,這里我主要只挑兩個(gè)和我們下一章實(shí)踐相關(guān)的插件:
          相關(guān)插件我們可以根據(jù)需要然后通過 pip 命令安裝即可,最后使用只需要簡單的參照插件的使用文檔編寫相應(yīng)的部分,最后啟動(dòng) pytest 測試即可。

          pytest-xdist

          pytest-xdist 是一個(gè)由 pytest 團(tuán)隊(duì)維護(hù),并能讓我們進(jìn)行并行測試以提高我們測試效率的 pytest 插件,因?yàn)槿绻覀兊捻?xiàng)目是有一定規(guī)模,那么測試的部分必然會(huì)很多。而由于 pytest 收集測試用例時(shí)是以一種同步的方式進(jìn)行,因此無法充分利用到多核。
          因此通過 pytest-xdist 我們就能大大加快每輪測試的速度。當(dāng)然我們只需要在啟動(dòng) pytest 測試時(shí)加上 -n <CPU_NUMBER> 參數(shù)即可,其中的 CPU 數(shù)量可以直接用 auto 代替,它會(huì)自動(dòng)幫你調(diào)整 pytest 測試所使用的 CPU 核心數(shù):

          pytest-asyncio

          pytest-asycnio 是一個(gè)讓 pytest 能夠測試異步函數(shù)或方法的擴(kuò)展插件,同樣是由 pytest 官方維護(hù)。由于目前大部分的異步框架或庫往往都是會(huì)基于 Python 官方的 asyncio 來實(shí)現(xiàn),因此 pytest-asyncio 可以進(jìn)一步在測試用例中集成異步測試和異步載具。
          我們直接在測試的函數(shù)或方法中直接使用 @pytest.mark.asyncio 標(biāo)記裝飾異步函數(shù)或方法,然后進(jìn)行測試即可:
          import asyncio

          import pytest


          async def foo():
               await asyncio.sleep(1)
               return 1

          @pytest.mark.asyncio
          async def test_foo():
              r = await foo()
              assert r == 1

          結(jié)語

          本次內(nèi)容主要簡單介紹了一下 pytest 概念及其核心特性,我們可以看到 pytest 在測試部分是多么易用。pytest 特性和使用示例遠(yuǎn)遠(yuǎn)不止于此,官方文檔已經(jīng)足夠全面,感興趣的朋友可以進(jìn)一步深入了解。
          下一部分內(nèi)容我們將會(huì)以 Web 項(xiàng)目為例進(jìn)一步集成 pytest 進(jìn)行實(shí)踐。

          作者:100gle,練習(xí)時(shí)長不到兩年的非正經(jīng)文科生一枚,喜歡敲代碼、寫寫文章、搗鼓搗鼓各種新事物;現(xiàn)從事有關(guān)大數(shù)據(jù)分析與挖掘的相關(guān)工作。


          贊 賞 作 者



          更多閱讀



          2020 年最佳流行 Python 庫 Top 10


          2020 Python中文社區(qū)熱門文章 Top 10


          5分鐘掌握 Python 對(duì)象的引用

          特別推薦




          點(diǎn)擊下方閱讀原文加入社區(qū)會(huì)員

          瀏覽 89
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩理论视频 | 天天操比 | 日皮视频在线免费观看 | 亚洲涩情91日韩一区二区 | 日韩欧美一级A片久久久一区S图 |