測試夾具簡介
在執行要測試的程式碼之前,有時需要設定更復雜的測試。可以在測試功能本身中執行此操作,但最終你需要執行大量測試功能,以至於很難確定設定停止和測試開始的位置。你還可以在各種測試功能之間獲得大量重複的設定程式碼。
我們的程式碼檔案:
# 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 夾具文件