pytest skip/xfail로 실패가 예상되는 테스트 다루기
pytest skip/xfail로 실패가 예상되는 테스트 다루기
특정 플랫폼에서 실행할 수 없거나 실패가 예상되는 테스트 함수를 마크하면, pytest가 그에 맞게 처리하고 테스트 스위트를 초록색으로 유지한 채 세션 요약을 보여줘요. skip은 어떤 조건이 충족돼야만 통과하는 테스트를, xfail은 어떤 이유로 실패할 걸로 예상하는 테스트를 표시하는 데 쓰입니다. 두 개념과 다양한 마크 인자를 자세히 살펴볼게요.
출처: How to use skip and xfail to deal with tests that cannot succeed — pytest 공식 문서
본문
skip은 조건이 충족될 때만 테스트가 통과할 것을 기대하고, 그렇지 않으면 pytest가 테스트를 아예 실행하지 않고 건너뛰게 하는 마크예요. 흔한 예로는 Windows 전용 테스트를 non-Windows 플랫폼에서 건너뛰거나, 현재 사용할 수 없는 외부 자원(예: 데이터베이스)에 의존하는 테스트를 건너뛰는 경우가 있어요.
xfail은 어떤 이유로 실패할 것을 기대하는 테스트예요. 아직 구현되지 않은 기능이나 고쳐지지 않은 버그에 대한 테스트가 흔한 예입니다. 실패할 것으로 예상(pytest.mark.xfail로 마크)됐는데도 테스트가 통과하면 xpass라 부르고 테스트 요약에 보고됩니다.
pytest는 skip과 xfail 테스트를 따로 세고 목록으로 보여줘요. 기본적으로 skip/xfail 테스트의 자세한 정보는 출력을 어지럽히지 않도록 보여주지 않는데, -r 옵션으로 테스트 진행에서 보이는 "짧은" 글자에 해당하는 상세 정보를 볼 수 있습니다.
pytest -rxXs # show extra info on xfailed, xpassed, and skipped tests
-r 옵션의 자세한 내용은 pytest -h로 확인할 수 있어요.
테스트 함수 건너뛰기
테스트 함수를 건너뛰는 가장 간단한 방법은 선택적 reason을 넘길 수 있는 skip 데코레이터로 마크하는 겁니다.
@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown(): ...
대안으로, 테스트 실행 중이나 셋업 중에 pytest.skip(reason) 함수를 호출해 명령형으로도 건너뛸 수 있어요.
def test_function():
if not valid_config():
pytest.skip("unsupported configuration")
명령형 방식은 import 시점에 skip 조건을 평가할 수 없을 때 유용합니다.
모듈 전체를 건너뛰려면 모듈 레벨에서 pytest.skip(reason, allow_module_level=True)를 쓰면 돼요.
import sys
import pytest
if not sys.platform.startswith("win"):
pytest.skip("skipping windows-only tests", allow_module_level=True)
skipif
조건에 따라 건너뛰고 싶다면 skipif를 쓰면 됩니다. Python 3.13보다 이른 인터프리터에서 실행될 때 건너뛰도록 마크하는 예시예요.
import sys
@pytest.mark.skipif(sys.version_info < (3, 13), reason="requires python3.13 or higher")
def test_function(): ...
조건이 수집 중에 True로 평가되면 테스트 함수는 건너뛰어지고, -rs를 쓸 때 지정한 reason이 요약에 나타나요.
skipif 마커는 모듈 간에 공유할 수 있습니다. 이 테스트 모듈을 보면:
# content of test_mymodule.py
import mymodule
minversion = pytest.mark.skipif(
mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required"
)
@minversion
def test_function(): ...
마커를 import해서 다른 테스트 모듈에서 재사용할 수 있어요.
# test_myothermodule.py
from test_mymodule import minversion
@minversion
def test_anotherfunction(): ...
큰 테스트 스위트라면 마커를 정의하는 파일 하나를 두고 테스트 스위트 전체에 일관되게 적용하는 게 보통 좋아요. 조건 문자열을 불리언 대신 쓸 수도 있지만, 모듈 간 공유가 쉽지 않아 주로 하위 호환성 때문에 지원됩니다.
클래스나 모듈의 모든 테스트 함수 건너뛰기
skipif 마커는 다른 마커처럼 클래스에도 쓸 수 있어요.
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
class TestPosixCalls:
def test_function(self):
"will not be setup or run under 'win32' platform"
조건이 True면 이 마커는 그 클래스의 각 테스트 메서드마다 skip 결과를 만들어요. 모듈의 모든 테스트 함수를 건너뛰고 싶다면 pytestmark 글로벌을 쓰면 됩니다.
# test_module.py
pytestmark = pytest.mark.skipif(...)
테스트 함수에 skipif 데코레이터를 여러 개 적용하면, skip 조건 중 하나라도 참이면 건너뛰어집니다.
파일이나 디렉터리 건너뛰기
때로는 파일이나 디렉터리 전체를 건너뛰어야 할 수 있어요(예: 테스트가 파이썬 버전별 기능에 의존하거나 pytest가 실행하길 원하지 않는 코드 포함). 이 경우 해당 파일·디렉터리를 수집에서 제외해야 합니다.
누락된 import 의존성으로 건너뛰기
pytest.importorskip를 모듈 레벨·테스트 안·테스트 셋업 함수에서 쓰면 import가 누락된 테스트를 건너뛸 수 있어요.
docutils = pytest.importorskip("docutils")
docutils를 여기서 import할 수 없으면 테스트는 skip 결과가 돼요. 라이브러리 버전으로도 건너뛸 수 있습니다.
docutils = pytest.importorskip("docutils", minversion="0.3")
버전은 지정된 모듈의 __version__ 속성에서 읽어옵니다.
요약
상황별로 모듈의 테스트를 건너뛰는 빠른 안내입니다.
-
모듈의 모든 테스트를 무조건 건너뛰기:
pytestmark = pytest.mark.skip("all tests still WIP") -
어떤 조건에 따라 모듈의 모든 테스트 건너뛰기:
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only") -
어떤 import가 없으면 모듈의 모든 테스트 건너뛰기:
pexpect = pytest.importorskip("pexpect")
XFail: 테스트 함수를 실패 예상으로 마크하기
xfail 마커로 테스트가 실패할 것을 기대한다고 표시할 수 있어요.
@pytest.mark.xfail
def test_function(): ...
이 테스트는 실행되지만 실패해도 트레이스백이 보고되지 않아요. 대신 터미널 보고에서 "expected to fail"(XFAIL) 또는 "unexpectedly passing"(XPASS) 섹션에 나열됩니다.
테스트 안이나 셋업 함수 안에서 명령형으로 XFAIL로 마크할 수도 있어요.
def test_function():
if not valid_config():
pytest.xfail("failing configuration (but should work)")
def test_function2():
import slow_module
if slow_module.slow_function():
pytest.xfail("slow_module taking too long")
이 두 예시는 조건을 모듈 레벨에서 확인하고 싶지 않은 상황(마크에 대해 조건을 평가해서는 안 되는 경우)을 보여줘요. pytest.xfail 호출은 내부적으로 알려진 예외를 일으켜 구현되기 때문에, 마커와 달리 호출 뒤의 다른 코드는 실행되지 않습니다.
condition 인자
특정 조건에서만 실패가 예상된다면 그 조건을 첫 번째 인자로 넘길 수 있어요.
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function(): ...
이때 reason도 함께 넘겨야 합니다.
reason 인자
예상 실패의 사유를 reason 인자로 지정할 수 있어요.
@pytest.mark.xfail(reason="known parser issue")
def test_function(): ...
raises 인자
실패 이유를 더 구체적으로 지정하려면 raises 인자에 단일 예외나 예외 튜플을 넘기면 됩니다.
@pytest.mark.xfail(raises=RuntimeError)
def test_function(): ...
raises에 언급되지 않은 예외로 실패하면 정규 실패로 보고돼요.
run 인자
xfail로 마크하고 그렇게 보고하되, 아예 실행조차 하지 않으려면 run 인자를 False로 쓰세요.
@pytest.mark.xfail(run=False)
def test_function(): ...
인터프리터를 죽이는 테스트를 xfail로 두고 나중에 조사할 때 특히 유용합니다.
strict 인자
기본적으로 XFAIL과 XPASS 모두 테스트 스위트를 실패시키지 않아요. strict 키워드 전용 인자를 True로 설정하면 바꿀 수 있습니다.
@pytest.mark.xfail(strict=True)
def test_function(): ...
이러면 이 테스트의 XPASS("unexpectedly passing") 결과가 테스트 스위트를 실패시키게 돼요. strict 인자의 기본값은 strict_xfail ini 옵션으로 바꿀 수 있습니다.
[pytest]
xfail_strict = true
[pytest]
strict_xfail = true
xfail 무시하기
명령줄에서 이렇게 지정하면:
pytest --runxfail
xfail 마크된 테스트를 마크가 없는 것처럼 강제로 실행·보고할 수 있어요. 이러면 pytest.xfail도 아무 효과가 없습니다.
예시
여러 사용법이 담긴 간단한 테스트 파일이에요. report-on-xfail 옵션으로 실행하면:
! pytest -rx xfail_demo.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR/example
collected 7 items
xfail_demo.py xxxxxxx [100%]
========================= short test summary info ==========================
XFAIL xfail_demo.py::test_hello
XFAIL xfail_demo.py::test_hello2
reason: [NOTRUN]
XFAIL xfail_demo.py::test_hello3
condition: hasattr(os, 'sep')
XFAIL xfail_demo.py::test_hello4
bug 110
XFAIL xfail_demo.py::test_hello5
condition: pytest.__version__[0] != "17"
XFAIL xfail_demo.py::test_hello6
reason: reason
XFAIL xfail_demo.py::test_hello7
============================ 7 xfailed in 0.12s ============================
파라미터화와 함께 쓰는 skip/xfail
파라미터화에서 개별 테스트 인스턴스에 skip·xfail 같은 마커를 적용할 수 있어요.
import sys
import pytest
@pytest.mark.parametrize(
("n", "expected"),
[
(1, 2),
pytest.param(1, 0, marks=pytest.mark.xfail),
pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
(2, 3),
(3, 4),
(4, 5),
pytest.param(
10, 11, marks=pytest.mark.skipif(sys.version_info >= (3, 0), reason="py2k")
),
],
)
def test_increment(n, expected):
assert n + 1 == expected