1. 程式人生 > 實用技巧 >Pytest單元測試框架-學習

Pytest單元測試框架-學習

pytest: Python的一個單元測試框架,基於UnitTest二次開發,語法上更加簡潔,可以用來做Python開發專案的單元測試,UI自動化、介面自動化測試等,有很多的外掛訪問Pytest外掛彙總,對Pytest進行擴充套件。

pytest是一個框架,它使構建簡單且可伸縮的測試變得容易。測試具有表達性和可讀性,不需要樣板程式碼。在幾分鐘內開始對應用程式或庫進行小的單元測試或複雜的功能測試。 -- 來自Pytest官方文件(由谷歌翻譯)

https://docs.pytest.org/en/latest/parametrize.html

安裝

pip install pytest

檔名稱規則

與UnitTest類似,測試檔名稱需要以test_檔名.py

(推薦使用這種)或者檔名_test.py格式。

測試類的名稱:Test類名來定義一個測試類

測試方法(函式):test_方法名來定義一個測試方法

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_01.py
@ide: PyCharm
@time: 2020/7/27
"""
# 測試類TestNumber
class TestNumber(object):
    def __int__(self):
        pass

    def test_sum(self):
        # assert進行斷言
        assert 1 == 2

    def test_str(self):
      	# isinstance 判斷物件是否為對應型別
        assert isinstance(1, int)

執行測試

方法1:在當前目錄下使用命令列,輸入pytest將會執行該目錄下所有test_*.py檔案

(venv) bogon:pytest_study zy7y$ pytest
========================================================================= test session starts =========================================================================
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/zy7y/PycharmProjects/pytest_study
collected 3 items                                                                                                                                                     

test_01.py F.             # F.為測試失敗                                                                                                                                      [ 66%]
test_02.py . 	# . 為測試通過                                                                                                                                                   [100%]

============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestNumber.test_sum _________________________________________________________________________

self = <test_01.TestNumber object at 0x1110fb580>

    def test_sum(self):
        # assert進行斷言
>       assert 1 == 2
E       assert 1 == 2

test_01.py:19: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_01.py::TestNumber::test_sum - assert 1 == 2
===================================================================== 1 failed, 2 passed in 0.07s =====================================================================

方法2: 在當前目錄下使用命令列,輸入pytest test_*.py指定執行某個測試檔案

(venv) bogon:pytest_study zy7y$ pytest test_01.py
F.                                                                                                                                                              [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestNumber.test_sum _________________________________________________________________________

self = <test_01.TestNumber object at 0x10824aac0>

    def test_sum(self):
        # assert進行斷言
>       assert 1 == 2
E       assert 1 == 2

test_01.py:19: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_01.py::TestNumber::test_sum - assert 1 == 2
1 failed, 1 passed in 0.06s

方法3:在測試檔案下使用pytest.main()方法來執行.

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_01.py
@ide: PyCharm
@time: 2020/7/27
"""


# 測試類TestNumber
class TestNumber(object):
    def __int__(self):
        pass

    def test_sum(self):
        # assert進行斷言
        assert 1 == 2

    def test_str(self):
        assert isinstance(1, int)


if __name__ == '__main__':
    import pytest
    # 相當於在命令行當前目錄中執行了 pytest
    pytest.main()

    # 執行pytest -q test_01.py : -q 簡短輸出測試資訊
    pytest.main(['-q', 'test_01.py'])

方法4:使用Pycharm修改單元測試框架來啟動單個測試檔案.

![image-20200727155705065](/Users/zy7y/Library/Application Support/typora-user-images/image-20200727155705065.png)

![image-20200727155814350](/Users/zy7y/Library/Application Support/typora-user-images/image-20200727155814350.png)

臨時目錄

pytest提供內建的fixtures / function引數來請求任意資源,例如唯一的臨時目錄:

在測試函式簽名中列出名稱tmpdir,pytest將在執行測試函式呼叫之前查詢並呼叫夾具工廠來建立資源。 在測試執行之前,pytest建立一個“每次測試呼叫唯一”臨時目錄

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_02.py
@ide: PyCharm
@time: 2020/7/27
"""

def test_02():
    assert 1 == 1


# 臨時目錄
def test_neddsfiles(tmpdir):
    # 列印臨時目錄地址
    print(tmpdir)   # tmpdir = local('/private/var/folders/m4/gq5zy91j62x8qx_msq5mdbxw0000gn/T/pytest-of-zy7y/pytest-7/test_neddsfiles0')

    # 0 和 None 、 '' 在python中屬於 False
    assert None

pytest其他命令

命令列中執行:
pytest --fixtures   # 顯示內建和自定義 fixtures
pytest -h		# 檢視幫助
pytest -x 	# 第一次故障後停止測試
pytest --maxfail=2  # 2次失敗後停止

執行測試方法:
pytest test_*.py	# 在模組中執行測試(執行整個.py下的測試類、方法、函式)
pytest testing/ 	# 在工作目錄執行測試(執行指定檔案目錄下的所有測試類、方法、函式)

pytest -k "Number and not str" 	# 關鍵字執行
這將執行包含與給定字串表示式(大小寫不敏感)匹配的名稱的測試,其中可以包括使用檔名、類名和函式名作為變數的Python操作符。上面的示例將執行TestNumber。測試一些東西,但不執行test_str測試方法。

pytest test_02.py::test_02 # 在模組中執行測試,就是說執行test_02.py檔案中test_02的函式

pytest test_01.py::TestNumber::test_sum # 執行模組下指定測試類的測試方法

pytest -m slow  # 將執行所有用@pytest.mark.slow裝飾的測試

pytest --pyargs pkg.testing	# 執行檔名稱為testing的包下面所有的測試檔案

fixture(依賴注入)

注意: Pytest對於每個fixture只會快取一個例項,這意味著如果使用引數化的fixture,pytest可能會 比定義的作用域更多次的呼叫fixture函式(因為需要建立不同引數的fixture)

  • 作為函式入參的fixture

    """
    @project: pytest_study
    @author: zy7y
    @file: test_03.py
    @ide: PyCharm
    @time: 2020/7/27
    """
    
    import pytest
    
    
    # 其他測試函式傳入這個函式名稱時會得到一個例項物件,且一直都是這個例項物件
    @pytest.fixture
    def get_driver():
        from selenium import webdriver
        # 返回一個操控Chrome瀏覽器的驅動例項 driver
        driver = webdriver.Chrome()
        return driver
    
    
    def test_browse(get_driver):
        get_driver.get("http://www.baidu.com")
        print(id(get_driver))
    
    
    def test_close(get_driver):
        get_driver.close()
        print(id(get_driver))
    
  • 在類/模組/中共享fixture例項

    將fixture函式放在獨立的conftest.py檔案中,可以達到在多個測試模組中訪問使用這個fixture;將scope="module"引數新增到@pytest.fixture中

    # conftest.py
    #!/usr/bin/env/python3
    # -*- coding:utf-8 -*-
    """
    @project: pytest_study
    @author: zy7y
    @file: conftest.py
    @ide: PyCharm
    @time: 2020/7/27
    """
    
    
    import pytest
    @pytest.fixture(scope='module')
    def browse_driver():
        from selenium import webdriver
        # 返回一個操控Chrome瀏覽器的驅動例項 driver
        driver = webdriver.Chrome()
        return driver
    
    # test_04.py
    #!/usr/bin/env/python3
    # -*- coding:utf-8 -*-
    """
    @project: pytest_study
    @author: zy7y
    @file: test_04.py
    @ide: PyCharm
    @time: 2020/7/27
    """
    
    
    def test_open_browse(browse_driver):
        browse_driver.get("https://www.baidu.com")
    
    
    def test_open_blog(browse_driver):
        import time
        time.sleep(3)
        browse_driver.get("https://www.baidu.com")
        print(browse_driver.window_handles)
        browse_driver.close()
    
  • fixture結束/執行清理程式碼

    # 修改後的conftest.py
    import pytest
    @pytest.fixture(scope='module')
    def browse_driver():
        # from selenium import webdriver
        # # 返回一個操控Chrome瀏覽器的驅動例項 driver
        # driver = webdriver.Chrome()
        # return driver
    
        # 呼叫結束/執行清理程式碼
        from selenium import webdriver
        driver = webdriver.Chrome()
        # 呼叫前 會例項一個 driver, 然後通過yield 返回
        yield driver
        # 用例結束後使用close關閉driver
        driver.close()
    
    # test_05.py
    #!/usr/bin/env/python3
    # -*- coding:utf-8 -*-
    """
    @project: pytest_study
    @author: zy7y
    @file: test_05.py
    @ide: PyCharm
    @time: 2020/7/27
    """
    
    
    def test_open_browse(browse_driver):
        browse_driver.get("https://www.baidu.com")
        assert browse_driver.title == '百度一下,你就知道'
    
    
    def test_open_blog(browse_driver):
        import time
        time.sleep(3)
        browse_driver.get("https://www.cnblogs.com/zy7y/")
        assert 'zy7y' in browse_driver.title
    
  • fixture韌體作用域

    在定義韌體時,通過 scope 引數宣告作用域,可選項有: function: 函式級,每個測試函式都會執行一次韌體; class: 類級別,每個測試類執行一次,所有方法都可以使用; module: 模組級,每個模組執行一次,模組內函式和方法都可使用; session: 會話級,一次測試只執行一次,所有被找到的函式和方法都可用。 package,表示fixture函式在測試包(資料夾)中第一個測試用例執行前和最後一個測試用例執行後執行一次

    # 啟動順序 session(單元測試集 -> 模組 -> 類 - >方法)

用例初始化(UnitTest的類前執行等方法)

模組級別(setup_module/teardown_module) 全域性的 函式級別(setup_function/teardown_function) 只對函式用例生效(不在類中的) 類級別 (setup_class/teardown_class) 只在類中前後執行一次(只在類中才生效) 方法級別 (setup_method/teardown_method) 開始後與方法始末(在類中) 類內用例初始化結束 (setup/teardown) 執行在測試用例的前後

import pytest
 
def setup_module():
    print('\nsetup_module 執行')
 
def teardown_module():
    print('\nteardown_module 執行')
 
def setup_function():
    """函式方法(類外)初始化"""
    print('setup_function 執行')
 
def teardown_function():
    """函式方法(類外)初始化"""
    print('\nteardown_function 執行')
 
def test_in_class():
    """類(套件)外的測試用例"""
    print('類外測試用例')
 
class Test_clazz(object):
    """測試類"""
 
    def setup_class(self):
        print('setup_class 執行')
 
    def teardown_class(self):
        print('teardown_class 執行')
 
    def setup_method(self):
        print('setup_method 執行')
 
    def teardown_method(self0):
        print('teardown_method 執行')
 
    def setup(self):
        print('\nsetup 執行')
 
    def teardown(self):
        print('\nteardown 執行')
 
    def test_case001(self):
        """測試用例一"""
        print('測試用例一')
 
    def test_case002(self):
        """測試用例二"""
        print('測試用例二')

跳過測試函式

實現部分場景該測試函式不需要執行

# test_07.py
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_07.py
@ide: PyCharm
@time: 2020/7/27
"""
import pytest
import sys

# 這將強制跳過
@pytest.mark.skip(reason="該測試函式將被跳過")
def test_sum():
    assert 1 != 2


def test_count():
    assert 1 == 0


# 滿足條件才跳過
@pytest.mark.skipif(sys.version_info < (3, 6), reason="版本低於3.6將跳過不執行!")
def test_num():
    assert 1 + 3 == 5


# 標記測試用例期望失敗
@pytest.mark.xfail(reason='期望這個測試是失敗的')
def test_func01():
    assert 1 == 1

引數化-UnitTest的DDT

內建的pytest.mark.parametrize裝飾器可以用來對測試函式進行引數化處理。

#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: pytest_study
@author: zy7y
@file: test_08.py
@ide: PyCharm
@time: 2020/7/27
"""

import pytest

"""
'username, password, expect' 來傳遞 需要使用到的行參名
[('admin', '123456', '登入成功'), ('admin', '111111', '密碼錯誤')] 來傳遞對應的實參,這個列表中每一個元組執行一次
ids=["正常登入測試用例標題", "密碼錯誤登入測試用例"] 來傳遞對應每一次測試的用例標題
這個檔案中執行了兩次
"""

@pytest.mark.parametrize('username, password, expect',
                         [('admin', '123456', '登入成功'),
                          ('admin', '111111', '密碼錯誤')], ids=["正常登入測試用例標題", "密碼錯誤登入測試用例"])
def test_login(username, password, expect):
    if username == 'admin' and password == '123456':
        assert expect == '登入成功'
    else:
        assert expect == '密碼錯誤'


# 這將實現兩個裝飾器的引數組合並且用來測試
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
    assert x + y == 5

執行指令碼,會發生一個字元編碼的錯誤:

解決方法是在 專案根目錄下建立一個pytest.ini檔案並寫入

[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True