測試夾具簡介

在執行要測試的程式碼之前,有時需要設定更復雜的測試。可以在測試功能本身中執行此操作,但最終你需要執行大量測試功能,以至於很難確定設定停止和測試開始的位置。你還可以在各種測試功能之間獲得大量重複的設定程式碼。

我們的程式碼檔案:

# projectroot/module/stuff.py
class Stuff(object):
    def prep(self):
        self.foo = 1
        self.bar = 2

我們的測試檔案:

# projectroot/tests/test_stuff.py
import pytest
from module import stuff

def test_foo_updates():
    my_stuff = stuff.Stuff()
    my_stuff.prep()
    assert 1 == my_stuff.foo
    my_stuff.foo = 30000
    assert my_stuff.foo == 30000

def test_bar_updates():
    my_stuff = stuff.Stuff()
    my_stuff.prep()
    assert 2 == my_stuff.bar
    my_stuff.bar = 42
    assert 42 == my_stuff.bar

這些都是非常簡單的例子,但是如果我們的 Stuff 物件需要更多的設定,那就會變得笨拙。我們看到在我們的測試用例之間存在一些重複的程式碼,所以讓我們首先將它重構為一個單獨的函式。

# projectroot/tests/test_stuff.py
import pytest
from module import stuff

def get_prepped_stuff():
    my_stuff = stuff.Stuff()
    my_stuff.prep()
    return my_stuff

def test_foo_updates():
    my_stuff = get_prepped_stuff()
    assert 1 == my_stuff.foo
    my_stuff.foo = 30000
    assert my_stuff.foo == 30000

def test_bar_updates():
    my_stuff = get_prepped_stuff()
    assert 2 == my_stuff.bar
    my_stuff.bar = 42
    assert 42 == my_stuff.bar

這看起來更好但我們仍然讓 my_stuff = get_prepped_stuff() 呼叫混亂了我們的測試功能。

py.test 固定裝置救援!

夾具是功能強大且靈活的測試設定功能版本。他們可以做的比我們在這裡利用的要多得多,但我們會一步一個腳印。

首先,我們將 get_prepped_stuff 更改為名為 prepped_stuff 的夾具。你想用名詞而不是動詞來命名你的燈具,因為燈具將在以後最終用於測試功能。@pytest.fixture 表示該特定功能應作為夾具而不是常規功能處理。

@pytest.fixture
def prepped_stuff():
    my_stuff = stuff.Stuff()
    my_stuff.prep()
    return my_stuff

現在我們應該更新測試功能,以便他們使用夾具。這是通過在其定義中新增一個與夾具名稱完全匹配的引數來完成的。當 py.test 執行時,它將在執行測試之前執行 fixture,然後通過該引數將 fixture 的返回值傳遞給測試函式。 (請注意,燈具不需要返回值;它們可以改為執行其他設定操作,例如呼叫外部資源,在檔案系統上安排內容,將值放入資料庫,無論設定需要什麼測試)

def test_foo_updates(prepped_stuff):
    my_stuff = prepped_stuff
    assert 1 == my_stuff.foo
    my_stuff.foo = 30000
    assert my_stuff.foo == 30000

def test_bar_updates(prepped_stuff):
    my_stuff = prepped_stuff
    assert 2 == my_stuff.bar
    my_stuff.bar = 42
    assert 42 == my_stuff.bar

現在你可以看到為什麼我們用名詞命名它。但是 my_stuff = prepped_stuff 系列幾乎沒用,所以讓我們直接使用 prepped_stuff 代替。

def test_foo_updates(prepped_stuff):
    assert 1 == prepped_stuff.foo
    prepped_stuff.foo = 30000
    assert prepped_stuff.foo == 30000

def test_bar_updates(prepped_stuff):
    assert 2 == prepped_stuff.bar
    prepped_stuff.bar = 42
    assert 42 == prepped_stuff.bar

現在我們正在使用燈具! 我們可以通過改變燈具的範圍來進一步改進(因此每個測試模組或測試套件執行會話只執行一次,而不是每個測試功能執行一次),構建使用其他燈具的燈具,引數化燈具(以便燈具和所有燈具)使用該燈具的測試執行多次,對於給予燈具的每個引數執行一次),從呼叫它們的模組讀取值的燈具……如前所述,燈具比普通設定功能具有更多的功率和靈活性。

測試完成後清理

假設我們的程式碼已經增長,我們的 Stuff 物件現在需要特殊清理。

# projectroot/module/stuff.py
class Stuff(object):
def prep(self):
    self.foo = 1
    self.bar = 2

def finish(self):
    self.foo = 0
    self.bar = 0

我們可以新增一些程式碼來呼叫每個測試函式底部的清理,但是 fixtures 提供了一種更好的方法來實現這一點。如果向夾具新增一個函式並將其註冊為終結器,則在使用夾具完成測試後,將呼叫終結器函式中的程式碼。如果 fixture 的範圍大於單個函式(如模組或會話),則在範圍內的所有測試完成後執行終結器,因此在模組完成執行後或在整個測試執行會話結束時。

@pytest.fixture
def prepped_stuff(request):  # we need to pass in the request to use finalizers
    my_stuff = stuff.Stuff()
    my_stuff.prep()
    def fin():  # finalizer function
        # do all the cleanup here
        my_stuff.finish()
    request.addfinalizer(fin)  # register fin() as a finalizer
    # you can do more setup here if you really want to
    return my_stuff

使用函式內部的終結器功能乍一看有點難以理解,特別是當你有更復雜的燈具時。你可以使用 yield fixture 來使用更易讀的執行流來執行相同的操作。唯一真正的區別是,我們不是使用 return,而是在燈具的一部分使用 yield 進行設定,控制應該進入測試功能,然後在 yield 之後新增所有清理程式碼。我們還將它裝飾成 yield_fixture,以便 py.test 知道如何處理它。

@pytest.yield_fixture
def prepped_stuff():  # it doesn't need request now!
    # do setup
    my_stuff = stuff.Stuff()
    my_stuff.prep()
    # setup is done, pass control to the test functions
    yield my_stuff
    # do cleanup 
    my_stuff.finish()

這就是測試夾具簡介的結論!

有關更多資訊,請參閱官方 py.test fixture 文件官方 yield 夾具文件