Pytest 教學
Posted on Mar 22, 2016 in Python 程式設計 - 中階 by Amo Chen ‐ 4 min read
建議本文的閱讀者可以先看過 Python unittest 了解基本觀念之後再閱讀本文。
俗話說的好:「不學測試,無以立。」想寫得一手好程式就從測試著手。
雖然 Python 內建 unittest,但畢竟還是有些許不便之處。基於不重新造輪的哲學,就來學一套坊間人人稱頌的pytest 吧!
以下是官方宣稱加上個人自身體驗 pytest 幾點很棒的地方:
Helpful traceback and failing assertion reporting
Print debugging and the capturing of standard output during test execution
Parametrizeable fixtures - pytest can run nose, unittest and doctest style test suites, including running testcases made for Django and trial
簡而言之就是方便、順手、好用。
在開始前要先提一下, pytest 如何執行測試,
pytest 遵循標準的 test discovery rules ,幾點要領:
- 檔名必須是符合
test_*.py
或*_test.py
- 類別名稱必須是
`Test`
開頭 - 函數與類別內的方法都必須要
test_
做為 prefix
只要至少遵守上述 3 個要領, pytest 就可以自動幫你執行所有合乎規定的測試。
直接來看怎麼用 pytest 吧!
例如:
test_hello_pytest.py
# -*- coding: utf-8 -*-
def test_hello_pytest():
assert 1 == 2
$ py.test test_hello_pytest.py
================== test session starts ==================
platform linux2 -- Python 2.7.9, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
rootdir: /home/user/test_hello_pytest.py, inifile:
collected 1 items
test_hello_pytest.py F
======================= FAILURES ========================
___________________ test_hello_pytest ___________________
def test_hello_pytest():
> assert 1 == 2
E assert 1 == 2
test_hello_pytest.py:4: AssertionError
=============== 1 failed in 0.01 seconds ================
沒錯,其實與一般的測試並沒有兩樣,但是多了更方便的 failure report 可以清楚地知道測試哪裡沒有通過。
如果介意檔案中都是一堆測試函數的話,也可以直接用 unittest 撰寫,或者用類別將測試進行分組。
例如:
class TestGroup:
def test_h_in_hello(self):
assert 'h' in 'hello'
def test_str_has_split_method(self):
assert hasattr('str', 'split')
pytest Fixtures
pytest 的 Fixture 跟 unittest 最不一樣的地方是 pytest 可以自動把 fixture 的名字帶到測試函數中,例如 20 個測試函數都需要同樣的 fixture 的話,就可以直接在測試函數寫上與該 fixture 同名的參數, pytest 就會自動幫忙代入 fixture 。
# -*- coding: utf-8 -*-
import pytest
@pytest.fixture()
def hello_world():
return "hello_world"
def test_h_in_hello_world(hello_world):
assert "h" in hello_world
def test_w_in_hello_world(hello_world):
assert "w" in hello_world
共享 Fixture
測試中常見的就是 fixture 蠻有可能會被共用的,例如資料庫連線可以在測試中被共用,那麼這個資料庫連線就可以被提升為共享的 fixture ,方便大家 import 使用。
pytest 中,只要把共享的 fixture 集中放在 conftest.py
,並加上 scope
參數即可。
例如:
conftest.py
# -*- coding: utf-8 -*-
import pytest
class DB(object):
def __init__(self):
print id(self)
@pytest.fixture(scope="module")
def db():
return DB()
test_db.py
# -*- coding: utf-8 -*-
def test_db_a(db):
assert 1 == 2
def test_db_b(db):
assert 1 == 2
$ py.test test_db.py
================= test session starts ==================
platform linux2 -- Python 2.7.9, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
rootdir: /home/user/test_db.py, inifile:
collected 2 items
test_db.py FF
======================= FAILURES ========================
_______________________ test_db_a _______________________
db = <conftest.DB object at 0x7f6a13c10610>
def test_db_a(db):
> assert 1 == 2
E assert 1 == 2
test_db.py:3: AssertionError
----------------- Captured stdout setup -----------------
140093574678032
_______________________ test_db_b _______________________
db = <conftest.DB object at 0x7f6a13c10610>
def test_db_b(db):
> assert 1 == 2
E assert 1 == 2
test_db.py:6: AssertionError
=============== 2 failed in 0.01 seconds ================
上述例子中可以看到 scope="module"
, fixture scope 還有 function, module, session 之分。
- function 每個 test 所使用的 fixture 都是不一樣的。如果以網路連線的 fixture 為例,每次執行測試都會重開一個網路連線。
- module 同一個 module 裡的測試會用相同的 fixture。如果以網路連線 fixture 為例,相同 module 的測試都是共用同一個網路連線 fixture 。例如上述範例可以發現
db = <conftest.DB object at 0x7f6a13c10610>
記憶體位置都是一樣的,代表都是使用到相同的 fixture 。 - session 同一次測試裡的 fixture 都是一樣的。如果以網路連線 fixture 為例,所有測試都是使用同一個網路連線 fixture 。
請善用 scope ,除了節省資源之外,還可以加速測試。
內建的 Fixtures
pytest 有提供一些內建的 fixture 減少重造輪子的情況,以下指令可以列出內建的 fixture
$ py.test --fixtures
詳細的內建 fixture 就不多做解釋了,可以到官方網站查詢。
Parametrizing a fixture
有些時候可能 fixture 需要針對多種不同的參數進行測試,例如測試不同的 server ip/domain ,這時可以選擇 parametrizing a fixture , pytest 就會自動幫忙針對不同的 fixture 參數執行測試,就不用額外多寫個測試了。
以 test_action
為例子, pytest 自動以 start, stop 各自進行一次測試,完全不需要額外多撰寫一個測試。
test_parametrizing_test.py
# -*- coding: utf-8 -*-
import pytest
@pytest.fixture(scope='module', params=['start', 'stop'])
def action(request):
return request.param
def test_action(action):
assert 'pause' == action
$ py.test test_parametrizing_test.py
================== test session starts ==================
platform linux2 -- Python 2.7.9, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
rootdir: /home/user/test_action.py, inifile:
collected 2 items
test_action.py FF
======================= FAILURES ========================
__________________ test_action[start] ___________________
action = 'start'
def test_action(action):
> assert 'pause' == action
E assert 'pause' == 'start'
E - pause
E + start
test_action.py:8: AssertionError
___________________ test_action[stop] ___________________
action = 'stop'
def test_action(action):
> assert 'pause' == action
E assert 'pause' == 'stop'
E - pause
E + stop
test_action.py:8: AssertionError
=============== 2 failed in 0.02 seconds ================
Custom Mark
pytest 還支援將不同的測試標上不同的標籤,讓你可以利用標籤將測試分組,使你可只執行某個標籤相關的所有測試就好。
test_mark.py
# -*- coding: utf-8 -*-
import pytest
@pytest.mark.my_mark
def test_a():
assert 1 == 1
@pytest.mark.not_my_mark
def test_b():
assert 2 == 2
$ py.test -m my_mark
================== test session starts ==================
platform linux2 -- Python 2.7.9, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
rootdir: /home/user/test_mark.py, inifile:
collected 2 items
test_mark.py .
========= 1 tests deselected by "-m 'my_mark'" ==========
======== 1 passed, 1 deselected in 0.00 seconds ========
結合 pdb (python debugger)
此外, pytest 也整合了 pdb ,可以在測試沒通過的時候,直接進入 pdb ,可以很方便的找出測試失敗的原因。
只要在執行測試時多加 --pdb
的參數即可:
$ py.test --pdb mytest.py
事實上, pytest 還提供不少方便的功能,建議可以到 py.test 官網詳細閱讀,一定可以收穫不少!