建議本文的閱讀者可以先看過 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 官網詳細閱讀,一定可以收穫不少!

Reference

http://pytest.org/