pytest로 stdout/stderr 출력 캡처하기

pytest로 stdout/stderr 출력 캡처하기

pytest는 --capture= 명령줄 인자나 픽스처로 stdout과 stderr를 가로채요. --capture= 플래그는 보고 방식을 설정하고, 픽스처는 더 세밀한 제어와 테스트 중 출력 검사를 가능하게 합니다. 기본 캡처 덕분에 print 디버깅도 쉽고, capsys 같은 픽스처로 출력 값을 직접 검증할 수도 있어요. 여기서는 캡처 방식과 접근 방법을 살펴볼게요.

출처: How to capture stdout/stderr output — pytest 공식 문서

본문

기본 stdout/stderr/stdin 캡처 동작

테스트 실행 중 stdoutstderr로 보내진 모든 출력이 캡처돼요. 테스트나 셋업 메서드가 실패하면 해당 캡처된 출력이 보통 실패 트레이스백과 함께 표시됩니다(이 동작은 --show-capture 명령줄 옵션으로 설정 가능). 추가로 stdin은 "null" 객체로 설정돼 읽기 시도가 실패하는데, 자동 테스트 실행에서 대화형 입력을 기다리는 건 바람직하지 않기 때문이에요.

기본적으로 캡처는 낮은 수준의 파일 디스크립터에 대한 쓰기를 가로채는 방식으로 이뤄져요. 그래서 단순한 print 문 출력뿐 아니라 테스트가 시작한 서브프로세스의 출력도 캡처할 수 있습니다.

캡처 방식 설정과 비활성화

pytest가 캡처를 수행하는 방식은 세 가지예요.

  • fd(파일 디스크립터) 레벨 캡처(기본값): 운영체제 파일 디스크립터 1과 2로 가는 모든 쓰기가 캡처돼요.
  • sys 레벨 캡처: 파이썬 파일 sys.stdoutsys.stderr로 가는 쓰기만 캡처해요. 파일 디스크립터로의 쓰기는 캡처되지 않습니다.
  • tee-sys 캡처: sys.stdoutsys.stderr로의 파이썬 쓰기를 캡처하되, 실제 sys.stdout·sys.stderr로도 통과시켜요. 이러면 출력을 '실시간 출력'하면서 junitxml 같은 플러그인용으로 캡처할 수 있어요(pytest 5.4의 새 기능).

명령줄에서 출력 캡처 메커니즘을 조절할 수 있어요.

pytest -s                  # disable all capturing
pytest --capture=sys       # replace sys.stdout/stderr with in-mem files
pytest --capture=fd        # also point filedescriptors 1 and 2 to temp file
pytest --capture=tee-sys   # combines 'sys' and '-s', capturing sys.stdout/stderr
                           # and passing it along to the actual sys.stdout/stderr

디버깅에 print 문 사용하기

stdout/stderr 출력의 기본 캡처가 주는 주요 이점 중 하나는 디버깅에 print 문을 쓸 수 있다는 거예요.

# content of test_module.py


def setup_function(function):
    print("setting up", function)


def test_func1():
    assert True


def test_func2():
    assert False

이 모듈을 실행하면 실패한 함수의 출력만 정확히 보여주고 다른 쪽은 숨겨요.

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_module.py .F                                                    [100%]

================================= FAILURES =================================
________________________________ test_func2 ________________________________

    def test_func2():
>       assert False
E       assert False

test_module.py:12: AssertionError
-------------------------- Captured stdout setup ---------------------------
setting up <function test_func2 at 0xdeadbeef0001>
========================= short test summary info ==========================
FAILED test_module.py::test_func2 - assert False
======================= 1 failed, 1 passed in 0.12s ========================

테스트 함수에서 캡처된 출력 접근하기

capsys, capteesys, capsysbinary, capfd, capfdbinary 픽스처는 테스트 실행 중 생성된 stdout/stderr 출력에 접근하게 해줘요.

출력 관련 검사를 수행하는 예시 테스트 함수:

def test_myoutput(capsys):  # or use "capfd" for fd-level
    print("hello")
    sys.stderr.write("world\n")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    assert captured.err == "world\n"
    print("next")
    captured = capsys.readouterr()
    assert captured.out == "next\n"

readouterr() 호출은 지금까지의 출력을 스냅샷 찍고, 캡처는 계속돼요. 테스트 함수가 끝나면 원래 스트림이 복원됩니다. 이렇게 capsys를 쓰면 출력 스트림의 설정/해제를 신경 쓸 필요가 없어지고 pytest의 테스트별 캡처와도 잘 어울려요. readouterr()의 반환값은 outerr 두 속성을 가진 namedtuple입니다.

테스트 대상 코드가 텍스트가 아닌 데이터(bytes)를 쓴다면, capsysbinary 픽스처로 캡처할 수 있어요. 이 픽스처는 readouterr 메서드에서 bytes를 반환합니다. 파일 디스크립터 레벨에서 캡처하고 싶다면 capfd 픽스처를 쓰면 돼요. 이 픽스처는 같은 인터페이스를 제공하면서 OS 레벨 출력 스트림(FD1과 FD2)에 직접 쓰는 라이브러리나 서브프로세스의 출력도 캡처할 수 있어요. capsysbinary와 마찬가지로 capfdbinary를 파일 디스크립터 레벨에서 bytes를 캡처하는 데 쓸 수 있습니다.

테스트 안에서 캡처를 임시로 끄려면 캡처 픽스처의 disabled() 메서드를 컨텍스트 매니저로 쓰면 됩니다. with 블록 안에서 캡처가 꺼져요.

def test_disabling_capturing(capsys):
    print("this output is captured")
    with capsys.disabled():
        print("output not captured, going directly to sys.stdout")
    print("this output is also captured")

capsys·capfd 같은 캡처 픽스처를 쓰면 -s--capture=no 같은 명령줄 옵션으로 설정한 전역 캡처 설정보다 우선해요. 즉 캡처 픽스처를 쓰는 테스트 안에서 생성된 출력은 전역 캡처가 꺼져 있어도 여전히 캡처되고 readouterr()로 접근할 수 있습니다.

더 알아보기