1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 11482字,需瀏覽 23分鐘

         ·

        2021-04-18 18:42

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

        為什么要做單元測試

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

        所以,為了能讓測試變得簡單且具備可擴展性,一個名為 pytest 的測試框架在 Python 社區(qū)中誕生了,使用 pytest 我們可以不用考慮如何基于 TestCase 來實現(xiàn)我們的測試,我們只需要簡單到保持我們原有的代碼邏輯不變,外加一個 assert 關鍵字來斷言結果,剩下的部分 pytest 會幫我們處理。
        # 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()
        之后我們只需要運行包含上述代碼的 main.py 文件,就能在終端控制臺上看到 pytest 為我們測試得到的結果。如果結果通過,則不會有過多的信息顯示,如果測試失敗,則會拋出錯誤信息并告知運行時 data 里的內(nèi)容是什么。
        盡管說 pytest 已經(jīng)足夠簡單,但它也提供了許多實用的功能(如:依賴注入),這些功能本身是存在著一些概念層面的知識;但這并不意味著勸退想要使用 pytest 來測試自己代碼的人,而是讓我們擁有更多的選擇,因此只有對 pytest 的這些功能及其概念有了更好地了解,我們才能夠充分發(fā)揮 pytest 的威力。

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

        通過 pip install pytest 安裝 pytest 之后,我們就可以快速實現(xiàn)我們的第一個測試。
        首先我們可以任意新建一個 Python 文件,這里我直接以 test_main.py 命名,然后當中留存如下內(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
        之后將終端切換到該文件所處路徑下,然后運行 pytest -v,就會看到 pytest 已經(jīng)幫我們將待測試的參數(shù)傳入到測試函數(shù)中,并實現(xiàn)對應的結果:

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

        Pytest 概念與用法

        命名

        如果需要 pytest 對你的代碼進行測試,首先我們需要將待測試的函數(shù)、類、方法、模塊甚至是代碼文件,默認都是以 test_* 開頭或是以 *_test 結尾,這是為了遵守標準的測試約定。如果我們將前面快速上手的例子文件名中的 test_ 去掉,就會發(fā)現(xiàn) pytest 沒有收集到對應的測試用例。
        當然我們也可以在 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
        但通常情況下我們使用默認的 test 前后綴即可。如果我們只想挑選特定的測試用例或者只對特定模塊下的模塊進測試,那么我們可以在命令行中通過雙冒號的形式進行指定,就像這樣:
        pytest test.py::test_demo
        pytest test.py::TestDemo::test_demo

        標記(mark)

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

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

        正如前面的示例以及它的命名意思一樣,mark.parametrize 主要就是用于我們想傳遞不同參數(shù)或不同組合的參數(shù)到一個待測試對象上的這種場景。
        正如我們前面的 test_add() 示例一樣,分別測試了:
        • x=1y=1 時,結果是否為 result=2 的情況
        • x=2y=4 時,結果是否為 result=6 的情況
        • x=3.3y=3 時,結果是否為 result=6.3 的情況
        • ……
        我們也可以將參數(shù)堆疊起來進行組合,但效果也是類似:
        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
        當然如果我們有足夠多的參數(shù),只要寫進了 parametrize 中,pytest 依舊能幫我們把所有情況都給測試一遍。這樣我們就再也不用寫多余的代碼。
        但需要注意的是,parametrize 和我們后面將要講到的一個重要的概念 fixture 會有一些差異:前者主要是模擬不同參數(shù)下時待測對象會輸出怎樣的結果,而后者是在固定參數(shù)或數(shù)據(jù)的情況下,去測試會得到怎樣的結果。

        跳過測試

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

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

        預期異常

        代碼只要是人寫的必然會存在不可避免的 BUG,當然有一些 BUG 我們作為寫代碼的人是可以預期得到的,這類特殊的 BUG 通常也叫異常(Exception)。比如我們有一個除法函數(shù):
        def div(x, y):
            return x / y
        但根據(jù)我們的運算法則可以知道,除數(shù)不能為 0;因此如果我們傳遞 y=0 時,必然會引發(fā) ZeroDivisionError 異常。所以通常的做法要么就用 try...exception 來捕獲異常,并且拋出對應的報錯信息(我們也可以使用 if 語句進行條件判斷,最后也同樣是拋出報錯):
        def div(x, y):
            try:
                return x/y
            except ZeroDivisionError:
                raise ValueError("y 不能為 0")
        因此,此時在測試過程中,如果我們想測試異常斷言是否能被正確拋出,此時就可以使用 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,而非前者。當然我們可以使用另外一個標記化的方法(pytest.mark.xfail)來和 pytest.mark.parametrize 相結合:

        @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
        這樣測試過程中會直接標記出失敗的部分。

        Fixture

        在 pytest 的眾多特性中,最令人感到驚艷的就是 fixture。關于 fixture 的翻譯大部分人都直接將其直譯為了「夾具」一詞,但如果你有了解過 Java Spring 框架的 那么你在實際使用中你就會更容易將其理解為 IoC 容器類似的東西,但我自己認為它叫「載具」或許更合適。
        因為通常情況下都是 fixture 的作用往往就是為我們的測試用例提供一個固定的、可被自由拆裝的通用對象,本身就像容器一樣承載了一些東西在里面;讓我們使用它進行我們的單元測試時,pytest 會自動向載具中注入對應的對象。
        這里我稍微模擬了一下我們在使用使用數(shù)據(jù)庫時的情況。通常我們會通過一個數(shù)據(jù)庫類創(chuàng)建一下數(shù)據(jù)庫對象,然后使用前先進行連接 connect(),接著進行操作,最后使用完之后斷開連接 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
        在這段代碼中,實現(xiàn)載具的關鍵是 @pytest.fixture 這一行裝飾器代碼,通過該裝飾器我們可以直接使用一個帶有資源的函數(shù)將其作為我們的載具,在使用時將函數(shù)的簽名(即命名)作為參數(shù)傳入到我們的測試用例中,在運行測試時 pytest 則會自動幫助我們進行注入。

        在注入的過程中 pytest 會幫我們執(zhí)行 myclient()db 對象的 connect() 方法調(diào)用模擬數(shù)據(jù)庫連接的方法,在測試完成之后會再次幫我們調(diào)用 close() 方法釋放資源。
        pytest 的 fixture 機制是一個讓我們能實現(xiàn)復雜測試的關鍵,試想我們以后只需要寫好一個帶有測試數(shù)據(jù)的 fixture,就可以在不同的模塊、函數(shù)或者方法中多次使用,真正做到「一次生成,處處使用」。
        當然 pytest 給我們提供了可調(diào)節(jié)載具作用域(scope)的情況,從小到大依次是:
        • function:函數(shù)作用域(默認)
        • class:類作用域
        • module:模塊作用域
        • package:包作用域
        • session:會話作用域
        載具會隨著作用域的生命周期而誕生、銷毀。所以如果我們希望創(chuàng)建的載具作用域范圍增加,就可以在 @pytest.fixture() 中多增加一個 scope 參數(shù),從而提升載具作用的范圍。
        雖然 pytest 官方為我們提供了一些內(nèi)置的通用載具,但通常情況下我們自己自定義的載具會更多一些。所以我們都可以將其放到一個名為 conftest.py 文件中進行統(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()
        因為我們聲明了作用域為同一個包,那么在同一個包下我們再將前面的 test_add() 測試部分稍微修改一下,無需顯式導入 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
        之后運行 pytest -vs 即可看到輸出的結果:

        Pytest 擴展

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

        pytest-xdist

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

        pytest-asyncio

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

        結語

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

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            女技师三级做爰按摩 | 三级电影天堂 | 日本精品久久久久中文字幕 | 无码内射视频 | 欧美另类色 | 日本免费A∨ | 久久艹综合 | 偷拍视频一区二区 | 6080午夜伦理 | 亚洲精品区 |