在Python語言系中,有很多可用的自動化測試架構,比如早期大多數人會選用 unittest+HTMLTestRunner、Nose等,最近幾年比較常用的有Robot Framework,Robot Framework它是Python下一款非常通用的測試架構,采用擴充插件的機制可以幫助我們實作幾乎任何類型的自動化測試工作,如接口自動化測試、App自動化測試、Web UI自動化測試等,而針對Robot Framework架構系統性的使用和講解,筆者年初出版上市過一本《自動化測試實戰寶典》一書,感興趣的,可參閱此書:重磅消息 |《自動化測試實戰寶典:從小工到專家》隆重上市!。
今天本文重點介紹在Python語言下,另外一款通用的測試架構Pytest,雖說作為Robot Framework架構一書的作者去介紹Pytest,貌似不太合理,但架構技術本是一家,能快速解決實際問題的架構就是好架構,在年初的時候,也發表過一篇關于Robot Framework與Pytest架構選擇的一些建議: 聊一聊:Robot Framework被誤會多年的秘密,感興趣的讀者可以看看。
一句話總結:Pytest核心思路和Robot Framework大體一樣,可以通過插件擴充的形式,來滿足不同場景下的自動化測試需求。
1. Pytest介紹
Pytest是一個非常成熟的全功能的Python測試架構,與python自帶的unittest測試架構類似,但是比unittest架構使用起來更簡潔,功能更強大。
提供完善的線上文檔,并有着大量的第三方插件和内置幫助,适用于許多小型或大型項目。适合簡單的單元測試到複雜的功能測試。還可以執行 nose, unittest 和 doctest 風格的測試用例。支援良好的內建實踐, 支援擴充的 xUnit 風格 setup,支援非 Python 測試。支援生成測試覆寫率報告,支援 PEP8 相容的編碼風格。
2. Pytest安裝及基本使用
Pytest安裝非常簡單,可以通過 pip 指令直接線上安裝:
pip install -U pytest
Pytest 官方文檔:https://docs.pytest.org/en/latest/
安裝好之後,調用 pytest測試腳本方式:
1、py.test:
Pytest 提供直接調用的指令行工具,即 py.test,最新版本 pytest 和 py.test 兩個指令行工具都可用
2、python -m pytest:
效果和 py.test 一樣, 這種調用方式在多 Python 版本測試的時候是有用的, 例如測試 Python3:
python3 -m pytest [...]
基本文法:
usage: py.test [options] [file_or_dir] [file_or_dir] [...]
部分參數介紹:
py.test --version 檢視版本
py.test --fixtures, --funcargs 檢視可用的 fixtures
pytest --markers 檢視可用的 markers
py.test -h, --help 指令行和配置檔案幫助
# 失敗後停止
py.test -x 首次失敗後停止執行
py.test --maxfail=2 兩次失敗之後停止執行
# 調試輸出
py.test -l, --showlocals 在 traceback 中顯示本地變量
py.test -q, --quiet 靜默模式輸出
py.test -v, --verbose 輸出更詳細的資訊
py.test -s 捕獲輸出, 例如顯示 print 函數的輸出
py.test -r char 顯示指定測試類型的額外摘要資訊
py.test --tb=style 錯誤資訊輸出格式
- long 預設的traceback資訊格式化形式
- native 标準庫格式化形式
- short 更短的格式
- line 每個錯誤一行
# 運作指定 marker 的測試
pytest -m MARKEXPR
# 運作比對的測試
py.test -k stringexpr
# 隻收集并顯示可用的測試用例,但不運作測試用例
py.test --collect-only
# 失敗時調用 PDB
py.test --pdb
3.Pytest用例執行
3.1 用例查找規則
如果不帶參數運作pytest,那麼其先從配置檔案(pytest.ini,tox.ini,setup.cfg)中查找配置項 testpaths 指定的路徑中的test case,如果沒有則從目前目錄開始查找,否則,指令行參數就用于目錄、檔案查找。查找的規則如下:
- 查找指定目錄中以 test 開頭的目錄
- 遞歸周遊目錄,除非目錄指定了不同遞歸
- 查找檔案名以 test_ 開頭的檔案
- 查找以 Test 開頭的類(該類不能有 init 方法)
- 查找以 test_ 開頭的函數和方法并進行測試
如果要從預設的查找規則中忽略查找路徑,可以加上 --ingore 參數,例如:
pytest --ignore=test_case/xxx.py
3.2 執行選擇用例
1、執行單個子產品中的全部用例:
py.test test_demo.py
2、執行指定路徑下的全部用例:
py.test somepath
3、執行字元串表達式中的用例:
py.test -k stringexpr
4、運作指定子產品中的某個用例,如運作 test_demo.py 子產品中的 test_func 測試函數:
pytest test_demo.py::test_func
5、運作某個類下的某個用例,如運作 TestClass 類下的 test_method 測試方法:
pytest test_demo.py::TestClass::test_method
4.Pytest Fxiture特性
fixture 是 pytest 特有的功能,它用 pytest.fixture 辨別,定義在函數前面。在編寫測試函數的時候,可以将此函數名稱做為傳入參數,pytest 将會以依賴注入方式,将該函數的傳回值作為測試函數的傳入參數。
pytest.fixture(scope='function', params=None, autouse=False, ids=None)
4.1 作為參數
fixture 可以作為其他測試函數的參數被使用,前提是其必須傳回一個值:
@pytest.fixture()
def hello():
return "hello"
def test_string(hello):
assert hello == "hello", "fixture should return hello"
4.2 作為 setup
fixture 也可以不傳回值,這樣可以用于在測試方法運作前運作一段代碼:
@pytest.fixture() # 預設參數,每個測試方法前調用
def before():
print('before each test')
def test_1(before):
print('test_1()')
@pytest.mark.usefixtures("before")
def test_2():
print('test_2()')
這種方式與 setup_method、setup_module 等的用法相同,其實它們也是特殊的 fixture。
在上例中,有一個測試用了 pytest.mark.usefixtures裝飾器來标記使用哪個 fixture,這中用法表示在開始測試前應用該 fixture 函數但不需要其傳回值。
4.3 fixture作用範圍
fixtrue 可以通過設定 scope 參數來控制其作用域(同時也控制了調用的頻率)。如果 scope='module',那麼 fixture 就是子產品級的,這個 fixture 函數隻會在每次相同子產品加載的時候執行。這樣就可以複用一些需要時間進行建立的對象。fixture 提供四種作用域,用于指定 fixture 初始化的規則:
- function:每個測試函數之前執行一次,預設
- class: 每個類之前執行一次,
- module:每個子產品加載之前執行一次
- session:每次 session 之前執行一次,即每次測試執行一次
4.4 反向請求
fixture 函數可以通過接受 request 對象來反向擷取請求中的測試函數、類或子產品上下文。例如:
@pytest.fixture(scope="module")
def smtp(request):
import smtplib
server = getattr(request.module, "smtpserver", "smtp.qq.com")
smtp = smtplib.SMTP(server, 587, timeout=5)
yield smtp
smtp.close()
有時需要全面測試多種不同條件下的一個對象,功能是否符合預期。可以通過設定 fixture 的 params 參數,然後通過 request 擷取設定的值:
class Foo(object):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def echo(self):
print(self.a, self.b, self.c)
return True
@pytest.fixture(params=[["1", "2", "3"], ["x", "y", "z"]])
def foo(request):
return Foo(*request.param)
def test_foo(foo):
assert foo.echo()
設定 params 參數後,運作 test 時将生成不同的測試 id,可以通過 ids 自定義 id:
@pytest.fixture(params=[1, 2, 4, 8], ids=["a", "b", "c", "d"])
def param_a(request):
return request.param
def test_param_a(param_a):
print param_a
4.5 setup/teardown
setup/teardown 是指在子產品、函數、類開始運作以及結束運作時執行一些動作。比如在一個函數中測試一個資料庫應用,測需要在函數開始前連接配接資料庫,在函數運作結束後斷開與資料庫的連接配接。setup/teardown 是特殊的 fixture,其可以有一下幾種實作方式:
# 子產品級别
def setup_module(module):
pass
def teardown_module(module):
pass
# 類級别
@classmethod
def setup_class(cls):
pass
@classmethod
def teardown_class(cls):
pass
# 方法級别
def setup_method(self, method):
pass
def teardown_method(self, method):
pass
# 函數級别
def setup_function(function):
pass
def teardown_function(function):
pass
有時候,還希望有全局的 setup 或 teardown,以便在測試開始時做一些準備工作,或者在測試結束之後做一些清理工作。這可以用 hook 來實作:
def pytest_sessionstart(session):
# setup_stuff
def pytest_sessionfinish(session, exitstatus):
# teardown_stuff
也可以用 fixture 的方式實作:
@fixture(scope='session', autouse=True)
def my_fixture():
# setup_stuff
yield
# teardown_stuff
4.6 自動執行
有時候需要某些 fixture 在全局自動執行,如某些全局變量的初始化工作,亦或一些全局化的清理或者初始化函數。這時可以通過設定 fixture 的 autouse 參數來讓 fixture 自動執行。設定為 autouse=True 即可使得函數預設執行。以下例子會在開始測試前清理可能殘留的檔案,接着将程式目錄設定為該目錄:
work_dir = "/c/temp"
@pytest.fixture(scope="session", autouse=True)
def clean_workdir():
shutil.rmtree(work_dir)
5. Pytest Mark特性
Pytest中marker 的作用是,用來标記測試,以便于選擇性的執行測試用例。Pytest 提供了一些内建的 marker:
# 跳過測試
@pytest.mark.skip(reason=None)
# 滿足某個條件時跳過該測試
@pytest.mark.skipif(condition)
# 預期該測試是失敗的
@pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False)
# 參數化測試函數。給測試用例添加參數,供運作時填充到測試中
# 如果 parametrize 的參數名稱與 fixture 名沖突,則會覆寫掉 fixture
@pytest.mark.parametrize(argnames, argvalues)
# 對給定測試執行給定的 fixtures
# 這種用法與直接用 fixture 效果相同
# 隻不過不需要把 fixture 名稱作為參數放在方法聲明當中
@pytest.mark.usefixtures(fixturename1, fixturename2, ...)
# 讓測試盡早地被執行
@pytest.mark.tryfirst
# 讓測試盡量晚執行
@pytest.mark.trylast
其中使用 pytest.skip 和 pytest.xfail 能夠實作跳過測試的功能,skip 表示直接跳過測試,而 xfail 則表示存在預期的失敗。
除了内建的 markers 外,pytest 還支援沒有實作定義的 markers,如:
@pytest.mark.old_test
def test_one():
assert False
@pytest.mark.new_test
def test_two():
assert False
@pytest.mark.windows_only
def test_three():
assert False
通過使用 -m 參數可以讓 pytest 選擇性的執行部分測試:
$ pytest test.py -m 'not windows_only'
更詳細的關于 marker 的說明可以參考官方文檔:https://docs.pytest.org/en/latest/example/markers.html
6. conftest.py檔案
從廣義了解,conftest.py 是一個本地的 per-directory 插件,在該檔案中可以定義目錄特定的 hooks 和 fixtures。py.test 架構會在它測試的項目中尋找 conftest.py 檔案,然後在這個檔案中尋找針對整個目錄的測試選項,比如是否檢測并運作 doctest 以及應該使用哪種模式檢測測試檔案和函數。
總結起來,conftest.py 檔案大緻有如下幾種功能:
- Fixtures: 用于給測試用例提供靜态的測試資料,其可以被所有的測試用于通路,除非指定了範圍。
- 加載插件: 用于導入外部插件或子產品:pytest_plugins ="myapp.testsupport.myplugin"
- 定義鈎子: 用于配置鈎子(hook),如 pytest_runtest_setup、pytest_runtest_teardown、pytest_config等。
- 測試根路徑: 如果将 conftest.py 檔案放在項目根路徑中,則 pytest 會自己搜尋項目根目錄下的子子產品,并加入到 sys.path 中,這樣便可以對項目中的所有子產品進行測試,而不用設定 PYTHONPATH 來指定項目子產品的位置。
test_case
├── conftest.py
├── module1
│ └── conftest.py
├── module2
│ └── conftest.py
└── module3
└── conftest.py
7. Pytest插件機制
- pytest-xdist: 分布式測試
- pytest-cov: 生成測試覆寫率報告
- pytest-pep8: 檢測代碼是否符合 PEP8 規範
- pytest-flakes: 檢測代碼風格
- pytest-html: 生成 html 報告
- pytest-randomly: 測試順序随機
- pytest-rerunfailures: 失敗重試
- pytest-timeout: 逾時測試