10、pytest -- skip和xfail標記

往期索引:https://blog.csdn.net/fyyaom/article/details/102840258

實際工作中,測試用例的執行可能會依賴於一些外部條件,例如:只能運行在某個特定的操作系統(Windows),或者我們本身期望它們測試失敗,例如:被某個已知的Bug所阻塞;如果我們能爲這些用例提前打上標記,那麼pytest就相應地預處理它們,並提供一個更加準確的測試報告;

在這種場景下,常用的標記有:

  • skip:只有當某些條件得到滿足時,才執行測試用例,否則跳過整個測試用例的執行;例如,在非Windows平臺上跳過只支持Windows系統的用例;
  • xfail:因爲一個確切的原因,我們知道這個用例會失敗;例如,對某個未實現的功能的測試,或者阻塞於某個已知Bug的測試;

pytest默認不顯示skipxfail用例的詳細信息,我們可以通過-r選項來自定義這種行爲;

通常,我們使用一個字母作爲一種類型的代表,具體的規則如下:

(f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed(p/P), or (A)ll

例如,顯示結果爲XFAILXPASSSKIPPED的用例:

pytest -rxXs

更多細節可以參考:2、使用和調用 – 總結報告

1. 跳過測試用例的執行

1.1. @pytest.mark.skip裝飾器

跳過執行某個用例最簡單的方式就是使用@pytest.mark.skip裝飾器,並且可以設置一個可選參數reason,表明跳過的原因;

@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
    ...

1.2. pytest.skip方法

如果我們想在測試執行期間(也可以在SetUp/TearDown期間)強制跳過後續的步驟,可以考慮pytest.skip()方法,它同樣可以設置一個參數msg,表明跳過的原因;

def test_function():
    if not valid_config():
        pytest.skip("unsupported configuration")

另外,我們還可以爲其設置一個布爾型的參數allow_module_level(默認是False),表明是否允許在模塊中調用這個方法,如果置爲True,則跳過模塊中剩餘的部分;

例如,在Windows平臺下,不測試這個模塊:

import sys
import pytest

if not sys.platform.startswith("win"):
    pytest.skip("skipping windows-only tests", allow_module_level=True)

注意:

當在用例中設置allow_module_level參數時,並不會生效;

def test_one():
    pytest.skip("跳出", allow_module_level=True)


def test_two():
    assert 1

也就是說,在上述示例中,並不會跳過test_two用例;

1.3. @pytest.mark.skipif裝飾器

如果我們想有條件的跳過某些測試用例的執行,可以使用@pytest.mark.skipif裝飾器;

例如,當python的版本小於3.6時,跳過用例:

import sys


@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
def test_function():
    ...

我們也可以在兩個模塊之間共享pytest.mark.skipif標記;

例如,我們在test_module.py中定義了minversion,表明當python的最低支持版本:

# src/chapter-10/test_module.py

import sys

import pytest

minversion = pytest.mark.skipif(sys.version_info < (3, 8),
                                reason='請使用 python 3.8 或者更高的版本。')


@minversion
def test_one():
    assert True

並且,在test_other_module.py中引入了minversion

# src/chapter-10/test_other_module.py

from test_module import minversion


@minversion
def test_two():
    assert True

現在,我們來執行這兩個用例(當前虛擬環境的python版本爲3.7.3):

λ pipenv run pytest -rs -k 'module' src/chapter-10/
================================ test session starts ================================= 
platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
rootdir: D:\Personal Files\Projects\pytest-chinese-doc
collected 2 items

src\chapter-10\test_module.py s                                                 [ 50%] 
src\chapter-10\test_other_module.py s                                           [100%]

============================== short test summary info =============================== 
SKIPPED [1] src\chapter-10\test_module.py:29: 請使用 python 3.8 或者更高的版本。
SKIPPED [1] src\chapter-10\test_other_module.py:26: 請使用 python 3.8 或者更高的版本。
================================= 2 skipped in 0.03s =================================

可以看到,minversion在兩個測試模塊中都生效了;

因此,在大型的測試項目中,可以在一個文件中定義所有的執行條件,需要時再引入到模塊中;

另外,需要注意的是,當一個用例指定了多個skipif條件時,只需滿足其中一個,就可以跳過這個用例的執行;

注意:不存在pytest.skipif()的方法;

1.4. pytest.importorskip方法

當引入某個模塊失敗時,我們同樣可以跳過後續部分的執行;

docutils = pytest.importorskip("docutils")

我們也可以爲其指定一個最低滿足要求的版本,判斷的依據是檢查引入模塊的__version__屬性:

docutils = pytest.importorskip("docutils", minversion="0.3") 

我們還可以再爲其指定一個reason參數,表明跳過的原因;

我們注意到pytest.importorskippytest.skip(allow_module_level=True)都可以在模塊的引入階段跳過剩餘部分;實際上,在源碼中它們拋出的都是同樣的異常:

# pytest.skip(allow_module_level=True)

raise Skipped(msg=msg, allow_module_level=allow_module_level)
# pytest.importorskip()

raise Skipped(reason, allow_module_level=True) from None

只是importorskip額外增加了minversion參數:

# _pytest/outcomes.py
 
if minversion is None:
        return mod
    verattr = getattr(mod, "__version__", None)
    if minversion is not None:
        if verattr is None or Version(verattr) < Version(minversion):
            raise Skipped(
                "module %r has __version__ %r, required is: %r"
                % (modname, verattr, minversion),
                allow_module_level=True,
            )

從中我們也證實了,它實際檢查的是模塊的__version__屬性;

所以,對於一般場景下,使用下面的方法可以實現同樣的效果:

try:
    import docutils
except ImportError:
    pytest.skip("could not import 'docutils': No module named 'docutils'",
                allow_module_level=True)

1.5. 跳過測試類

在類上應用@pytest.mark.skip@pytest.mark.skipif

# src/chapter-10/test_skip_class.py

import pytest


@pytest.mark.skip("作用於類中的每一個用例,所以 pytest 共收集到兩個 SKIPPED 的用例。")
class TestMyClass():
    def test_one(self):
        assert True

    def test_two(self):
        assert True

1.6. 跳過測試模塊

在模塊中定義pytestmark變量(推薦):

# src/chapter-10/test_skip_module.py

import pytest

pytestmark = pytest.mark.skip('作用於模塊中的每一個用例,所以 pytest 共收集到兩個 SKIPPED 的用例。')


def test_one():
    assert True


def test_two():
    assert True

或者,在模塊中調用pytest.skip方法,並設置allow_module_level=True

# src/chapter-10/test_skip_module.py

import pytest

pytest.skip('在用例收集階段就已經跳出了,所以不會收集到任何用例。', allow_module_level=True)


def test_one():
    assert True


def test_two():
    assert True

1.7. 跳過指定文件或目錄

通過在conftest.py中配置collect_ignore_glob項,可以在用例的收集階段跳過指定的文件和目錄;

例如,跳過當前測試目錄中文件名匹配test_*.py規則的文件和config的子文件夾sub中的文件:

collect_ignore_glob = ['test*.py', 'config/sub']

更多細節可以參考:https://docs.pytest.org/en/5.1.3/example/pythoncollection.html#customizing-test-collection

1.8. 總結

pytest.mark.skip pytest.mark.skipif pytest.skip pytest.importorskip conftest.py
用例 @pytest.mark.skip() @pytest.mark.skipif() pytest.skip(msg='') / /
@pytest.mark.skip() @pytest.mark.skipif() / / /
模塊 pytestmark = pytest.mark.skip() pytestmark = pytest.mark.skipif() pytest.skip(allow_module_level=True) pytestmark = pytest.importorskip() /
文件或目錄 / / / / collect_ignore_glob

2. 標記用例爲預期失敗的

我們可以使用@pytest.mark.xfail標記用例,表示期望這個用例執行失敗;

用例會正常執行,只是失敗時不再顯示堆棧信息,最終的結果有兩個:用例執行失敗時(XFAIL:符合預期的失敗)、用例執行成功時(XPASS:不符合預期的成功)

另外,我們也可以通過pytest.xfail方法在用例執行過程中直接標記用例結果爲XFAIL,並跳過剩餘的部分:

def test_function():
    if not valid_config():
        pytest.xfail("failing configuration (but should work)")

同樣可以爲pytest.xfail指定一個reason參數,表明原因;

下面我們來重點看一下@pytest.mark.xfail的用法:

  • condition位置參數,默認值爲None

    @pytest.mark.skipif一樣,它也可以接收一個python表達式,表明只有滿足條件時才標記用例;

    例如,只在pytest 3.6版本以上標記用例:

    @pytest.mark.xfail(sys.version_info >= (3, 6), reason="python3.6 api changes")
    def test_function():
        ...
    
  • reason關鍵字參數,默認值爲None

    可以指定一個字符串,表明標記用例的原因;

  • strict關鍵字參數,默認值爲False

    strict=False時,如果用例執行失敗,結果標記爲XFAIL,表示符合預期的失敗;如果用例執行成功,結果標記爲XPASS,表示不符合預期的成功;

    strict=True時,如果用例執行成功,結果將標記爲FAILED,而不再是XPASS了;

    我們也可以在pytest.ini文件中配置:

    [pytest]
    xfail_strict=true
    
  • raises關鍵字參數,默認值爲None

    可以指定爲一個異常類或者多個異常類的元組,表明我們期望用例上報指定的異常;

    如果用例的失敗不是因爲所期望的異常導致的,pytest將會把測試結果標記爲FAILED;

  • run關鍵字參數,默認值爲True:

    run=False時,pytest不會再執行測試用例,直接將結果標記爲XFAIL

我們以下表來總結不同參數組合對測試結果的影響(其中xfail = pytest.mark.xfail):

@xfail() @xfail(strict=True) @xfail(raises=IndexError) @xfail(strict=True, raises=IndexError) @xfail(..., run=False)
用例測試成功 XPASS FAILED XPASS FAILED XFAIL
用例測試失敗,上報AssertionError XFAIL XFAIL FAILED FAILED XFAIL
用例上報IndexError XFAIL XFAIL XFAIL XFAIL XFAIL

2.1. 去使能xfail標記

我們可以通過命令行選項pytest --runxfail來去使能xfail標記,使這些用例變成正常執行的用例,彷彿沒有被標記過一樣:

同樣,pytest.xfail()方法也將會失效;

3. 結合pytest.param方法

pytest.param方法可用於爲@pytest.mark.parametrize或者參數化的fixture指定一個具體的實參,它有一個關鍵字參數marks,可以接收一個或一組標記,用於標記這輪測試的用例;

我們以下面的例子來說明:

# src/chapter-10/test_params.py

import pytest
import sys


@pytest.mark.parametrize(
    ('n', 'expected'),
    [(2, 1),
     pytest.param(2, 1, marks=pytest.mark.xfail(), id='XPASS'),
     pytest.param(0, 1, marks=pytest.mark.xfail(raises=ZeroDivisionError), id='XFAIL'),
     pytest.param(1, 2, marks=pytest.mark.skip(reason='無效的參數,跳過執行')),
     pytest.param(1, 2, marks=pytest.mark.skipif(sys.version_info <= (3, 8), reason='請使用3.8及以上版本的python。'))])
def test_params(n, expected):
    assert 2 / n == expected

執行:

λ pipenv run pytest -rA src/chapter-10/test_params.py
================================ test session starts ================================= 
platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
rootdir: D:\Personal Files\Projects\pytest-chinese-doc
collected 5 items

src\chapter-10\test_params.py .Xxss                                             [100%]

======================================= PASSES ======================================= 
============================== short test summary info =============================== 
PASSED src/chapter-10/test_params.py::test_params[2-1]
SKIPPED [1] src\chapter-10\test_params.py:26: 無效的參數,跳過執行
SKIPPED [1] src\chapter-10\test_params.py:26: 請使用3.8及以上版本的python。
XFAIL src/chapter-10/test_params.py::test_params[XFAIL]
XPASS src/chapter-10/test_params.py::test_params[XPASS]
================= 1 passed, 2 skipped, 1 xfailed, 1 xpassed in 0.08s =================

關於參數化的fixture的細節可以參考:4、fixtures:明確的、模塊化的和可擴展的 – 在參數化的fixture中標記用例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章