Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Python 변환의 단위 테스트

Python 저장소는 pytest로 단위 테스트를 실행하고 CI checks에 결과를 포함할 수 있습니다. 이 저장소 테스트 경로는 배치 파이프라인을 대상으로 하며 스트리밍 파이프라인에는 지원하지 않습니다.

테스트는 실제 계산과 출력에 전달할 값을 검사하고, Foundry 데이터셋을 읽고 쓰는 핸들만 대체할 수 있습니다. CSV 입력과 기대 결과를 사용한 Polars, DuckDB, Pandas와 Spark 예제는 같은 필터를 실제 엔진에서 수행합니다.

CI 환경 설치

Checks의 condaPackRun은 해석한 환경의 패키지를 내려받아 압축 해제하고, checksum 또는 크기 검증 뒤 환경에 연결합니다. 패키지가 많을수록 설치 시간이 늘어납니다.

Run과 test의 환경 사양은 숨겨진 lockfile과 설치 캐시에 보관합니다. 설치 환경 캐시는 7일을 기준으로 사용하며 meta.yaml 변경 때 다시 구성합니다. 실제 잠금 파일의 이름과 재해석 조건은 사용 중인 템플릿에서 확인합니다.

테스트 플러그인

Transforms 저장소는 Python 하위 프로젝트의 build.gradle, Python library 저장소는 루트 build.gradle에 적용합니다.

Transforms 설정:

apply plugin: 'com.palantir.transforms.lang.python'
apply plugin: 'com.palantir.transforms.lang.python-defaults'
apply plugin: 'com.palantir.transforms.lang.pytest-defaults'

Library 설정:

apply plugin: 'com.palantir.transforms.lang.python-library'
apply plugin: 'com.palantir.transforms.lang.python-library-defaults'
apply plugin: 'com.palantir.transforms.lang.pytest-defaults'

condaLibraryPublish.onlyIf { versionDetails().commitDistance == 0 }

마지막 조건은 tag 이후 추가 commit이 없는 릴리스만 발행하는 예입니다. 테스트에서는 meta.yaml의 runtime 요구를 사용할 수 있고 추가 패키지는 Conda의 test.requires에 넣습니다.

로컬 Palantir 확장의 Palantir: Install Python environment로 .maestro를 만든 뒤 다음을 사용할 수 있습니다.

.maestro/bin/pytest

파일과 함수 발견

src/test 아래에 테스트를 두는 것이 권장됩니다. Pytest는 test_*.py 또는 *_test.py 파일의 test_ 함수들을 찾으며 Python unittest 방식의 테스트도 실행할 수 있습니다.

다음은 실패 메시지를 설명하기 위한 의도적인 실패 사례입니다. 정상 운영 테스트로 추가할 코드는 아닙니다.

def increment(number):
    return number + 1


def test_increment():
    assert increment(3) == 5

실제 계산은 4이고 기대값은 5이므로 실패하며 assertion 비교에서 assert 4 == 5를 확인할 수 있습니다.

DataFrame fixture의 기본 형태

다음은 두 행의 입력 프레임을 제공하는 pytest fixture입니다. 사용하는 엔진의 구현 하나를 선택하고 테스트 함수의 인자에 sample_frame을 선언하면 해당 프레임을 받을 수 있습니다. 업무 로직의 결과값은 아래의 CSV 테스트처럼 별도로 검사합니다.

Polars

import polars as pl
import pytest


@pytest.fixture
def sample_frame():
    return pl.DataFrame(
        [["a", 1], ["b", 2]], schema=["letter", "number"], orient="row"
    )

DuckDB

import duckdb
import pytest


@pytest.fixture
def sample_frame():
    with duckdb.connect() as connection:
        return connection.sql(
            "SELECT * FROM (VALUES ('a', 1), ('b', 2)) AS t(letter, number)"
        ).fetchdf()

Pandas

import pandas as pd
import pytest


@pytest.fixture
def sample_frame():
    return pd.DataFrame([["a", 1], ["b", 2]], columns=["letter", "number"])

Spark

import pytest


@pytest.fixture
def sample_frame(spark_session):
    return spark_session.createDataFrame(
        [["a", 1], ["b", 2]], ["letter", "number"]
    )

spark_session은 pytest-transforms가 제공하는 session 범위 fixture입니다. 테스트마다 새 session을 만드는 것이 아니므로 time zone 같은 설정을 바꾸면 다음 테스트에도 영향을 줍니다. 변경 전 값을 저장하고 테스트가 끝나기 전에 복원해야 합니다.

CSV 입력과 기대 결과

다음 두 파일을 저장소의 transforms-python/src/test/resources/에 둡니다.

업무 조건은 number_of_seats > 300이고 operating_status == "Yes"입니다. 300석인 EEE1과 운항하지 않는 BBB1은 제외하고 CCC1만 남습니다. 기대 파일은 계산 결과를 복사해 만드는 것이 아니라 이 조건에 맞춘 명시적 테스트 사례입니다.

Polars 테스트, DuckDB 테스트, Pandas 테스트, Spark 테스트에서 실제 계산과 출력 호출의 전체 값을 비교합니다. 각 계산 함수는 runtime에서도 같은 구현으로 등록하며 테스트가 비공개 decorator 내부 속성에 의존하지 않도록 구성합니다.

결과와 coverage

Checks에는 각 테스트가 PASSED, FAILED 또는 SKIPPED로 표시되고 기본적으로 접혀 있습니다. 펼치면 결과와 StdOut 및 StdErr를 볼 수 있습니다.

Coverage가 필요하면 기존 meta.yaml에 다음 test 요구를 병합합니다.

test:
  requires:
    - pytest-cov

transforms-python/src/pytest.ini에 대상 패키지와 기준을 지정합니다.

[pytest]
addopts = --cov=myproject --cov-report term --cov-fail-under=100

예시는 100% 미만이면 checks를 실패시킵니다. 실제 기준은 --cov-fail-under로 정하며 패키지 이름을 바꿨다면 myproject도 맞춥니다.

병렬 테스트

기본 실행은 순차적입니다. pytest-xdist 기반의 여러 프로세스를 사용하려면 플러그인을 적용한 뒤 numProcesses를 설정합니다.

apply plugin: 'com.palantir.transforms.lang.python'
apply plugin: 'com.palantir.transforms.lang.python-defaults'
apply plugin: 'com.palantir.transforms.lang.pytest-defaults'

tasks.pytest {
    numProcesses "3"
}

대기 테스트는 사용 가능한 worker에 배정되며 실행 순서가 보장되지 않습니다. 다른 테스트가 먼저 전역 상태를 바꿔 줄 것이라는 가정을 제거해야 합니다.

Style 및 Spark 검사

PEP8과 Pylint는 다음 플러그인으로 켤 수 있습니다.

apply plugin: 'com.palantir.conda.pep8'
apply plugin: 'com.palantir.conda.pylint'

Pylint 설정은 src/.pylintrc에서 관리합니다. 예를 들어 문서 문자열 메시지를 요구하지 않는 정책은 다음과 같습니다.

[MESSAGES CONTROL]
disable =
    missing-module-docstring,
    missing-function-docstring

모든 Pylint 설정이 Foundry에서 지원되는 것은 아닙니다. 실제 checks에 반영되지 않는 기능에 의존하지 않아야 합니다.

Spark의 정확성, 성능과 보안상 흔한 실수를 찾는 플러그인도 있습니다.

apply plugin: 'com.palantir.transforms.lang.antipattern-linter'

테스트가 켜지면 CI 로그에서 :transforms-python:pytest를 확인할 수 있습니다. 작은 예제 행은 Workbook 콘솔의 collect로 얻을 수 있지만 전체 큰 데이터를 드라이버로 모으지 않도록 합니다. PySpark schema는 Dataset Preview의 Columns > Copy > Copy PySpark Schema에서 가져올 수 있습니다.