pytest 임시 디렉터리·파일(tmp_path) 활용하기

pytest 임시 디렉터리·파일(tmp_path) 활용하기

테스트가 파일이나 디렉터리를 만들고 지우는 작업을 해야 한다면, 직접 임시 경로를 관리하느라 고생할 필요 없어요. pytest가 제공하는 tmp_path 픽스처가 테스트 함수마다 고유한 임시 디렉터리를 만들어주고, 테스트가 끝나면 알아서 정리해줍니다. 여기서는 tmp_path부터 세션 범위의 tmp_path_factory, 그리고 임시 디렉터리의 위치·보존 규칙까지 살펴볼게요.

출처: How to use temporary directories and files in tests — pytest 공식 문서

본문

tmp_path 픽스처

tmp_path 픽스처는 테스트 함수마다 고유한 임시 디렉터리를 제공해줘요. tmp_pathpathlib.Path 객체입니다. 사용 예시:

# content of test_tmp_path.py
CONTENT = "content"


def test_create_file(tmp_path):
    d = tmp_path / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text(CONTENT, encoding="utf-8")
    assert p.read_text(encoding="utf-8") == CONTENT
    assert len(list(tmp_path.iterdir())) == 1
    assert 0

실행하면 마지막 assert 0 줄 덕분에 값들을 확인하기 위해 실패한 테스트를 볼 수 있어요.

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

test_tmp_path.py F                                                   [100%]

================================= FAILURES =================================
_____________________________ test_create_file _____________________________

tmp_path = PosixPath('PYTEST_TMPDIR/test_create_file0')

    def test_create_file(tmp_path):
        d = tmp_path / "sub"
        d.mkdir()
        p = d / "hello.txt"
        p.write_text(CONTENT, encoding="utf-8")
        assert p.read_text(encoding="utf-8") == CONTENT
        assert len(list(tmp_path.iterdir())) == 1
>       assert 0
E       assert 0

test_tmp_path.py:11: AssertionError
========================= short test summary info ==========================
FAILED test_tmp_path.py::test_create_file - assert 0
============================ 1 failed in 0.12s =============================

기본적으로 pytest는 마지막 3번의 pytest 호출에 대한 임시 디렉터리를 보존해요. 같은 테스트 함수의 동시 실행은 각 동시 실행마다 고유한 기본 임시 디렉터리를 구성함으로써 지원됩니다. 상세는 temporary directory location and retention을 참고하세요.

tmp_path_factory 픽스처

tmp_path_factory는 세션 범위 픽스처로, 다른 어떤 픽스처나 테스트에서도 임의의 임시 디렉터리를 만들 때 쓸 수 있어요. 예를 들어 테스트 스위트가 절차적으로 생성되는 큰 이미지 파일을 필요로 한다고 해볼게요. 각 테스트가 자기 tmp_path에 같은 이미지를 계산하는 대신 세션당 한 번 생성해 시간을 아낄 수 있습니다.

# contents of conftest.py
import pytest


@pytest.fixture(scope="session")
def image_file(tmp_path_factory):
    img = compute_expensive_image()
    fn = tmp_path_factory.mktemp("data") / "img.png"
    img.save(fn)
    return fn


# contents of test_image.py
def test_histogram(image_file):
    img = load_image(image_file)
    # compute and test histogram

tmpdir·tmpdir_factory 픽스처

tmpdirtmpdir_factory 픽스처는 tmp_path·tmp_path_factory와 비슷하지만, 표준 pathlib.Path 객체 대신 레거시 py.path.local 객체를 쓰고 반환해요.

요즘은 tmp_pathtmp_path_factory를 쓰는 게 권장됩니다. 옛 코드 베이스를 현대화하는 데 도움을 주고 싶다면 legacypath 플러그인을 끈 상태로 pytest를 실행할 수 있어요.

pytest -p no:legacypath

이러면 레거시 경로를 쓰는 테스트에서 오류가 발생합니다. config 파일의 addopts 인자로 영구히 설정할 수도 있어요.

임시 디렉터리 위치와 보존

tmp_path와 (이제 deprecated된) tmpdir 픽스처가 반환하는 임시 디렉터리는 --basetemp 옵션에 따라 달라지는 구조로, 기본 임시 디렉터리 아래에 자동 생성돼요.

  • 기본값(--basetemp 옵션을 설정하지 않은 경우)의 임시 디렉터리는 다음 템플릿을 따릅니다.

    {temproot}/pytest-of-{user}/pytest-{num}/{testname}/
    

    여기서 각 항목은:

    • {temproot}: tempfile.gettempdir로 결정되는 시스템 임시 디렉터리. PYTEST_DEBUG_TEMPROOT 환경변수로 재정의할 수 있어요.
    • {user}: 테스트를 실행하는 사용자 이름
    • {num}: 테스트 스위트 실행 때마다 증가하는 숫자
    • {testname}: 현재 테스트 이름을 살균(sanitize)한 것

    자동 증가하는 {num} 자리 표시자는 기본적인 보존 기능을 제공해, 이전 테스트 실행 결과를 무작정 제거하지 않게 해줘요. 기본적으로 마지막 3개의 임시 디렉터리가 유지되는데, tmp_path_retention_counttmp_path_retention_policy로 설정할 수 있습니다.

  • --basetemp 옵션을 쓰면(예: pytest --basetemp=mydir) 그 값이 기본 임시 디렉터리로 직접 사용돼요.

    {basetemp}/{testname}/
    

    이 경우 보존 기능이 없다는 점에 주의하세요. 가장 최근 실행 결과만 유지됩니다.

    --basetemp에 준 디렉터리는 매 테스트 실행 전에 무조건 비워지므로, 그 목적으로만 쓸 디렉터리를 사용하세요.

pytest-xdist로 로컬 머신에서 테스트를 분산 실행할 때는 모든 임시 데이터가 테스트 실행 하나의 임시 디렉터리 아래에 모이도록 서브 프로세스용 basetemp 디렉터리를 자동으로 구성해줍니다.

더 알아보기