目錄
pytest
會將本輪測試的執行狀態寫入到.pytest_cache
文件夾,這個行爲是由自帶的cacheprovider
插件來實現的;
注意:
pytest
默認將測試執行的狀態寫入到根目錄中的.pytest_cache
文件夾,我們也可以通過在pytest.ini
中配置cache_dir
選項來自定義緩存的目錄,它可以是相對路徑,也可以是絕對路徑;相對路徑指的是相對於
pytest.ini
文件所在的目錄;例如,我們把這一章的緩存和源碼放在一起:在
src/chapter-12/pytest.ini
中添加如下配置:[pytest] cache_dir = .pytest-cache
這樣,即使我們在項目的根目錄下執行
src/chapter-12/
中的用例,也只會在pytest-chinese-doc/src/chapter-12/.pytest_cache
中生成緩存,而不再是pytest-chinese-doc/.pytest_cache
中;pytest-chinese-doc (5.1.3) λ pipenv run pytest src/chapter-12
1. cacheprovider
插件
在介紹這個插件之前,我們先看一個簡單例子:
# src/chapter-12/test_failed.py
import pytest
@pytest.mark.parametrize('num', [1, 2])
def test_failed(num):
assert num == 1
# src\chapter-12\test_pass.py
def test_pass():
assert 1
我們有兩個簡單的測試模塊,首先我們來執行一下它們:
λ pipenv run pytest -q src/chapter-12/
.F. [100%]
=============================== FAILURES ================================
____________________________ test_failed[2] _____________________________
num = 2
@pytest.mark.parametrize('num', [1, 2])
def test_failed(num):
> assert num == 1
E assert 2 == 1
src\chapter-12\test_failed.py:27: AssertionError
1 failed, 2 passed in 0.08s
可以看到一共收集到三個測試用例,其中有一個失敗,另外兩個成功的,並且兩個執行成功的用例分屬不同的測試模塊;
同時,pytest
也在src/chapter-12/
的目錄下生成緩存文件夾(.pytest_cache
),具體的目錄結構如下所示:
src
├───chapter-12
│ │ pytest.ini # 配置了 cache_dir = .pytest-cache
│ │ test_failed.py
│ │ test_pass.py
│ │
│ └───.pytest-cache
│ │ .gitignore
│ │ CACHEDIR.TAG
│ │ README.md
│ │
│ └───v
│ └───cache
│ lastfailed
│ nodeids
│ stepwise
現在,我們就結合上面的組織結構,具體介紹一下cacheprovider
插件的功能;
1.1. --lf, --last-failed
:只執行上一輪失敗的用例
緩存中的lastfailed
文件記錄了上次失敗的用例ID
,我們可以通過一下--cache-show
命令查看它的內容:
--cache-show
命令也是cacheprovider
提供的新功能,它不會導致任何用例的執行;
λ pipenv run pytest src/chapter-12/ -q --cache-show 'lastfailed'
cachedir: D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-12\.pytest-cache
--------------------- cache values for 'lastfailed' ---------------------
cache\lastfailed contains:
{'test_failed.py::test_failed[2]': True}
no tests ran in 0.01s
我們可以看到,它記錄了一個用例,爲上次失敗的測試用例的ID
:test_failed.py::test_failed[2]
;
下次執行時,當我們使用--lf
選項,pytest
在收集階段只會選擇這個失敗的用例,而忽略其它的:
λ pipenv run pytest --lf --collect-only src/chapter-12/
========================== test session starts ==========================
platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
cachedir: .pytest-cache
rootdir: D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-12, inifile: pytest.ini
collected 2 items / 1 deselected / 1 selected
<Module test_failed.py>
<Function test_failed[2]>
run-last-failure: rerun previous 1 failure (skipped 2 files)
========================= 1 deselected in 0.02s =========================
我們仔細觀察一下上面的回顯,有一句話可能會讓我們有點困惑:collected 2 items / 1 deselected / 1 selected
,可我們明明有三個用例,怎麼會只收集到兩個呢?
實際上,--lf
複寫了用例收集階段的兩個鉤子方法:pytest_ignore_collect(path, config)
和pytest_collection_modifyitems(session, config, items)
;
我們來先看看pytest_ignore_collect(path, config)
,如果它的結果返回True
,就忽略path
路徑中的用例;
# _pytest/cacheprovider.py
def last_failed_paths(self):
"""Returns a set with all Paths()s of the previously failed nodeids (cached).
"""
try:
return self._last_failed_paths
except AttributeError:
rootpath = Path(self.config.rootdir)
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
result = {x for x in result if x.exists()}
self._last_failed_paths = result
return result
def pytest_ignore_collect(self, path):
"""
Ignore this file path if we are in --lf mode and it is not in the list of
previously failed files.
"""
if self.active and self.config.getoption("lf") and path.isfile():
last_failed_paths = self.last_failed_paths()
if last_failed_paths:
skip_it = Path(path) not in self.last_failed_paths()
if skip_it:
self._skipped_files += 1
return skip_it
可以看到,如果當前收集的文件,不在上一次失敗的路徑集合內,就會忽略這個文件,所以這次執行就不會到test_pass.py
中收集用例了,故而只收集到兩個用例;並且pytest.ini
也在忽略的名單上,所以實際上是跳過兩個文件:(skipped 2 files)
;
至於pytest_collection_modifyitems(session, config, items)
鉤子方法,我們在下一節和--ff
命令一起看;
1.2. --ff, --failed-first
:先執行上一輪失敗的用例,再執行其它的
我們先通過實踐看看這個命令的效果,再去分析它的實現:
λ pipenv run pytest --collect-only -s --ff src/chapter-12/
========================== test session starts ==========================
platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
cachedir: .pytest-cache
rootdir: D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-12, inifile: pytest.ini
collected 3 items
<Module test_failed.py>
<Function test_failed[2]>
<Function test_failed[1]>
<Module test_pass.py>
<Function test_pass>
run-last-failure: rerun previous 1 failure first
========================= no tests ran in 0.02s =========================
我們可以看到一共收集到三個測試用例,和正常的收集順序相比,上一輪失敗的test_failed.py::test_failed[2]
用例在最前面,將優先執行;
實際上,-ff
只複寫了鉤子方法:pytest_collection_modifyitems(session, config, items)
,它可以過濾或者重新排序收集到的用例:
# _pytest/cacheprovider.py
def pytest_collection_modifyitems(self, session, config, items):
...
if self.config.getoption("lf"):
items[:] = previously_failed
config.hook.pytest_deselected(items=previously_passed)
else: # --failedfirst
items[:] = previously_failed + previously_passed
...
可以看到,如果使用的是lf
,就把之前成功的用例狀態置爲deselected
,這輪執行就會忽略它們;如果使用的是-ff
,只是將之前失敗的用例,順序調到前面;
另外,我們也可以看到lf
的優先級要高於ff
,所以它們同時使用的話,ff
是不起作用的;
1.3. --nf, --new-first
:先執行新加的或修改的用例,再執行其它的
緩存中的nodeids
文件記錄了上一輪執行的所有的用例:
λ pipenv run pytest src/chapter-12 --cache-show 'nodeids'
========================== test session starts ==========================
platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
cachedir: .pytest-cache
rootdir: D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-12, inifile: pytest.ini
cachedir: D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-12\.pytest-cache
---------------------- cache values for 'nodeids' -----------------------
cache\nodeids contains:
['test_failed.py::test_failed[1]',
'test_failed.py::test_failed[2]',
'test_pass.py::test_pass']
========================= no tests ran in 0.01s =========================
我們看到上一輪共執行了三個測試用例;
現在我們在test_pass.py
中新加一個用例,並修改一下test_failed.py
文件中的用例(但是不添加新用例):
# src\chapter-12\test_pass.py
def test_pass():
assert 1
def test_new_pass():
assert 1
現在我們再來執行一下收集命令:
λ pipenv run pytest --collect-only -s --nf src/chapter-12/
========================== test session starts ==========================
platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
cachedir: .pytest-cache
rootdir: D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-12, inifile: pytest.ini
collected 4 items
<Module test_pass.py>
<Function test_new_pass>
<Module test_failed.py>
<Function test_failed[1]>
<Function test_failed[2]>
<Module test_pass.py>
<Function test_pass>
========================= no tests ran in 0.03s =========================
可以看到,新加的用例順序在最前面,其次修改過的測試用例緊接其後,最後纔是舊的用例;這個行爲在源碼中有所體現:
# _pytest/cacheprovider.py
def pytest_collection_modifyitems(self, session, config, items):
if self.active:
new_items = OrderedDict()
other_items = OrderedDict()
for item in items:
if item.nodeid not in self.cached_nodeids:
new_items[item.nodeid] = item
else:
other_items[item.nodeid] = item
items[:] = self._get_increasing_order(
new_items.values()
) + self._get_increasing_order(other_items.values())
self.cached_nodeids = [x.nodeid for x in items if isinstance(x, pytest.Item)]
def _get_increasing_order(self, items):
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
item.fspath.mtime()
代表用例所在文件的最後修改時間,reverse=True
表明是倒序排列;
items[:] = self._get_increasing_order(new_items.values()) + self._get_increasing_order(other_items.values())
保證新加的用例永遠在最前面;
1.4. --cache-clear
:先清除所有緩存,再執行用例
直接看源碼:
# _pytest/cacheprovider.py
class Cache:
...
@classmethod
def for_config(cls, config):
cachedir = cls.cache_dir_from_config(config)
if config.getoption("cacheclear") and cachedir.exists():
rm_rf(cachedir)
cachedir.mkdir()
return cls(cachedir, config)
可以看到,它會先把已有的緩存文件夾刪除(rm_rf(cachedir)
),再創建一個空的同名文件夾(cachedir.mkdir()
),這樣會導致上述的功能失效,所以一般不使用這個命令;
1.5. 如果上一輪沒有失敗的用例
現在,我們清除緩存,再執行test_pass.py
模塊(它的用例都是能測試成功的):
λ pipenv run pytest --cache-clear -q -s src/chapter-12/test_pass.py
.
1 passed in 0.01s
這時候我們再去看一下緩存目錄:
.pytest-cache
└───v
└───cache
nodeids
stepwise
是不是少了什麼?對!因爲沒有失敗的用例,所以不會生成lastfailed
文件,那麼這個時候在使用--lf
和--ff
會發生什麼呢?我們來試試:
注意:
如果我們觀察的足夠仔細,就會發現現在的緩存目錄和之前相比不止少了
lastfailed
文件,還少了CACHEDIR.TAG
、.gitignore
和README.md
三個文件;這是一個
bug
,我已經在pytest 5.3.1版本
上提交了issue
,預計會在之後的版本修復,如果你有興趣深入瞭解一下它的成因和修復方案,可以參考這個:https://github.com/pytest-dev/pytest/issues/6290
luyao@NJ-LUYAO-T460 /d/Personal Files/Projects/pytest-chinese-doc (5.1.3)
λ pipenv run pytest -q -s --lf src/chapter-12/test_pass.py
.
1 passed in 0.01s
luyao@NJ-LUYAO-T460 /d/Personal Files/Projects/pytest-chinese-doc (5.1.3)
λ pipenv run pytest -q -s --ff src/chapter-12/test_pass.py
.
1 passed in 0.02s
可以看到,它們沒有實施任何影響;爲什麼會這樣?我們去源碼裏找一下答案吧;
# _pytest/cacheprovider.py
class LFPlugin:
""" Plugin which implements the --lf (run last-failing) option """
def __init__(self, config):
...
self.lastfailed = config.cache.get("cache/lastfailed", {})
...
def pytest_collection_modifyitems(self, session, config, items):
...
if self.lastfailed:
...
else:
self._report_status = "no previously failed tests, "
if self.config.getoption("last_failed_no_failures") == "none":
self._report_status += "deselecting all items."
config.hook.pytest_deselected(items=items)
items[:] = []
else:
self._report_status += "not deselecting items."
可以看到,當self.lastfailed
判斷失敗時,如果我們指定了last_failed_no_failures
選項爲none
,pytest
會忽略所有的用例(items[:] = []
),否則不做任何修改(和沒加--lf
或--ff
一樣),而判斷self.lastfailed
的依據是就是lastfailed
文件;
繼續看看,我們會學習到一個新的命令行選項:
# _pytest/cacheprovider.py
group.addoption(
"--lfnf",
"--last-failed-no-failures",
action="store",
dest="last_failed_no_failures",
choices=("all", "none"),
default="all",
help="which tests to run with no previously (known) failures.",
)
來試試吧:
λ pipenv run pytest -q -s --ff --lfnf none src/chapter-12/test_pass.py
1 deselected in 0.01s
λ pipenv run pytest -q -s --ff --lfnf all src/chapter-12/test_pass.py
.
1 passed in 0.01s
注意:
--lfnf
的實參只支持choices=("all", "none")
;
2. config.cache
對象
我們可以通過pytest
的config
對象去訪問和設置緩存中的數據;下面是一個簡單的例子:
# content of test_caching.py
import pytest
import time
def expensive_computation():
print("running expensive computation...")
@pytest.fixture
def mydata(request):
val = request.config.cache.get("example/value", None)
if val is None:
expensive_computation()
val = 42
request.config.cache.set("example/value", val)
return val
def test_function(mydata):
assert mydata == 23
我們先執行一次這個測試用例:
λ pipenv run pytest -q src/chapter-12/test_caching.py
F [100%]
================================ FAILURES =================================
______________________________ test_function ______________________________
mydata = 42
def test_function(mydata):
> assert mydata == 23
E assert 42 == 23
src/chapter-12/test_caching.py:43: AssertionError
-------------------------- Captured stdout setup --------------------------
running expensive computation...
1 failed in 0.05s
這個時候,緩存中沒有example/value
,將val
的值寫入緩存,終端打印running expensive computation...
;
查看緩存,其中新加了一個文件:.pytest-cache/v/example/value
;
.pytest-cache/
├── .gitignore
├── CACHEDIR.TAG
├── README.md
└── v
├── cache
│ ├── lastfailed
│ ├── nodeids
│ └── stepwise
└── example
└── value
3 directories, 7 files
通過--cache-show
選項查看,發現其內容正是42
:
λ pipenv run pytest src/chapter-12/ -q --cache-show 'example/value'
cachedir: /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-12/.pytest-cache
-------------------- cache values for 'example/value' ---------------------
example/value contains:
42
no tests ran in 0.00s
再次執行這個用例,這個時候緩存中已經有我們需要的數據了,終端就不會再打印running expensive computation...
:
λ pipenv run pytest -q src/chapter-12/test_caching.py
F [100%]
================================ FAILURES =================================
______________________________ test_function ______________________________
mydata = 42
def test_function(mydata):
> assert mydata == 23
E assert 42 == 23
src/chapter-12/test_caching.py:43: AssertionError
1 failed in 0.04s
3. Stepwise
試想一下,現在有這麼一個場景:我們想要在遇到第一個失敗的用例時退出執行,並且下次還是從這個用例開始執行;
以下面這個測試模塊爲例:
# src/chapter-12/test_sample.py
def test_one():
assert 1
def test_two():
assert 0
def test_three():
assert 1
def test_four():
assert 0
def test_five():
assert 1
我們先執行一下測試:pipenv run pytest --cache-clear --sw src/chapter-12/test_sample.py
;
λ pipenv run pytest --cache-clear --sw -q src/chapter-12/test_sample.py
.F
================================= FAILURES =================================
_________________________________ test_two _________________________________
def test_two():
> assert 0
E assert 0
src/chapter-12/test_sample.py:28: AssertionError
!!!!!! Interrupted: Test failed, continuing from this test next run. !!!!!!!
1 failed, 1 passed in 0.13s
使用--cache-clear
清除之前的緩存,使用--sw, --stepwise
使其在第一個失敗的用例處退出執行;
現在我們的緩存文件中lastfailed
記錄了這次執行失敗的用例,即爲test_two()
;nodeids
記錄了所有的測試用例;特殊的是,stepwise
記錄了最近一次失敗的測試用例,這裏也是test_two()
;
接下來,我們用--sw
的方式再次執行:pytest
首先會讀取stepwise
中的值,並將其作爲第一個用例開始執行;
λ pipenv run pytest --sw -q src/chapter-12/test_sample.py
F
================================= FAILURES =================================
_________________________________ test_two _________________________________
def test_two():
> assert 0
E assert 0
src/chapter-12/test_sample.py:28: AssertionError
!!!!!! Interrupted: Test failed, continuing from this test next run. !!!!!!!
1 failed, 1 deselected in 0.12s
可以看到,test_two()
作爲第一個用例開始執行,在第一個失敗處退出;
其實,pytest
還提供了一個--stepwise-skip
的命令行選項,它會忽略第一個失敗的用例,在第二個失敗處退出執行;我們來試一下:
λ pipenv run pytest --sw --stepwise-skip -q src/chapter-12/test_sample.py
F.F
=============================== FAILURES ================================
_______________________________ test_two ________________________________
def test_two():
> assert 0
E assert 0
src\chapter-12\test_sample.py:28: AssertionError
_______________________________ test_four _______________________________
def test_four():
> assert 0
E assert 0
src\chapter-12\test_sample.py:36: AssertionError
!!!!! Interrupted: Test failed, continuing from this test next run. !!!!! 2 failed, 1 passed, 1 deselected in 0.16s
這個時候,在第二個失敗的用例test_four()
處退出執行,同時stepwise
文件的值也改成了"test_sample.py::test_four"
;
其實,本章所有的內容都可以在源碼的
_pytest/cacheprovider.py
文件中體現,如果能結合源碼學習,會有事半功倍的效果;