Python에서의 속성 기반 테스트 (Property-Based Testing) 실전 가이드
요약
Python에서 Hypothesis 라이브러리를 활용한 속성 기반 테스트(Property-Based Testing) 방법을 소개합니다. 예시 기반 테스트의 한계를 넘어 코드의 불변량을 정의하고 엣지 케이스를 자동으로 찾아내는 실전 가이드를 제공합니다.
핵심 포인트
- 예시 기반 테스트와 달리 코드의 규칙(불변량)을 정의하여 테스트함
- Hypothesis 라이브러리를 사용하여 다양한 입력값과 엣지 케이스 자동 생성
- 데이터 변환 후 원상복구 시 정보가 보존되는 라운드트립 테스트 활용
- 멱등성(Idempotency) 검증을 통해 로직의 안정성 확보
Python에서의 속성 기반 테스트 (Property-Based Testing) 실전 가이드
Python에서의 속성 기반 테스트 (Property-Based Testing) 실전 가이드
속성 기반 테스트 (Property-based testing)는 몇 가지 예시 입력값을 직접 선정하는 대신, 코드가 항상 준수해야 하는 규칙 (rules) 을 기술함으로써 동작을 테스트하도록 도와줍니다. 이는 특히 엣지 케이스 (edge cases), 입력 검증 (input validation), 파싱 (parsing), 변환 (transformations), 그리고 예상치 못한 방식으로 실패할 수 있는 로직을 검증하는 데 매우 유용합니다.
이 접근 방식이 중요한 이유
전통적인 예시 기반 테스트 (example-based tests)는 확인하고자 하는 정확한 입력값과 출력값을 이미 알고 있을 때 유용합니다. 속성 기반 테스트는 "정렬을 해도 항목이 바뀌지 않아야 한다", "왕복 처리 (round-tripping) 시 데이터가 보존되어야 한다", 또는 "파싱 후 직렬화 (serializing)를 해도 정보가 손실되지 않아야 한다"와 같은 불변량 (invariants)으로 초점을 전환합니다.
이러한 특성 덕분에 가능한 입력값이 많은 코드에 매우 적합하며, 수동으로 작성할 생각을 하지 못했던 케이스들을 찾아낼 수 있습니다. 또한 항상 참이어야 하는 것이 무엇인지 정의하도록 유도하며, 이는 대개 더 명확한 요구사항으로 이어집니다.
무엇을 만들게 될 것인가
이 튜토리얼에서는 이메일 주소를 정규화 (normalize)하고 결제 금액을 처리하는 작은 Python 모듈을 테스트할 것입니다. 예제에서는 수많은 입력을 생성해주고, 실패하는 케이스를 최소한의 읽기 쉬운 형태로 축소 (shrink)해주는 속성 기반 테스트 라이브러리인 Hypothesis를 사용할 것입니다.
프로젝트 설정
Hypothesis와 pytest를 설치하세요:
pip install hypothesis pytest
app.py라는 이름의 파일을 생성하세요:
def normalize_email(email: str) -> str:
local, domain = email.strip().lower().split("@")
return f"{local}@{domain}"
...
이 코드는 의도적으로 단순하게 작성되었지만, 문자열 정규화 (string normalization)와 산술 불변량 (arithmetic invariants)이라는 두 가지 좋은 테스트 대상을 제공합니다. 속성 기반 테스트는 하나의 고정된 입력값보다는 값의 전체 범위를 확인하고 싶을 때 특히 효과적입니다.
첫 번째 속성 테스트
test_app.py를 생성하세요:
from hypothesis import given, strategies as st
from app import normalize_email
...
이 테스트는 광범위한 규칙을 명시합니다: 결과는 항상 소문자여야 합니다. Hypothesis는 다양한 텍스트 값의 조합을 시도할 것이며, 이는 몇 가지 예시를 수동으로 확인하는 것보다 더 효과적입니다.
라운드트립 (Round-trip) 동작 테스트
강력한 속성 중 하나는 라운드트립 (Round-tripping)입니다. 데이터를 한 방향으로 변환한 다음 다시 원래대로 변환했을 때, 원래의 의미를 복구할 수 있어야 합니다. 이메일 정규화 (Email normalization)의 경우, 유용한 속성은 정규화가 멱등성 (Idempotent)을 가져야 한다는 것입니다. 즉, 두 번 수행하는 것이 한 번 수행하는 것과 동일한 결과를 생성해야 함을 의미합니다.
from hypothesis import given, strategies as st
from app import normalize_email
...
이것은 특정 예시를 요구하지 않고도 중요한 동작 규칙을 확인하기 때문에 전형적인 속성입니다. 멱등성 (Idempotence)은 정규화 로직이 안정적이라는 것을 보여주는 좋은 신호인 경우가 많습니다.
경계값 (Boundaries) 테스트
산술 코드는 경계값 (Boundary-value) 사고방식의 이점을 얻습니다. 변이 테스트 (Mutation testing) 관련 문서들은 테스트가 오프 바이 원 (Off-by-one) 실수나 잘못된 연산자 변경을 잡아내야 한다고 자주 강조하며, 속성 기반 테스트 (Property-based tests)는 이러한 경계 (Edges)를 자동으로 실행하는 데 탁월합니다.
from hypothesis import given, strategies as st
from app import apply_discount
...
이 속성은 두 가지 중요한 사실을 동시에 확인합니다: 출력값은 음수가 될 수 없으며, 할인 (Discount)이 금액을 더 크게 만들어서는 안 된다는 것입니다. 이러한 종류의 불변량 (Invariant)은 이해하기 쉬우며, 취약한 테스트로는 속이기가 어렵습니다.
실제 버그 포착하기
만약 실수로 다음과 같이 작성했다고 가정해 봅시다:
def apply_discount(amount_cents: int, percent: int) -> int:
if amount_cents < 0:
raise ValueError("amount_cents must be non-negative")
...
몇 가지 예시 기반 테스트 (Example-based tests)는 단 하나의 해피 패스 (Happy path)만 확인한다면 이 버그를 놓칠 수도 있습니다. 하지만 위의 속성 기반 테스트는 결과가 원래 금액보다 커지게 되어 불변량 (Invariant)을 위반하므로 빠르게 실패할 것입니다.
좋은 속성 설계하기
좋은 속성 (Properties)은 대개 수많은 입력값에 걸쳐 유지되어야 하는 동작에 대한 단순한 진술입니다. 예를 들어 “출력값이 정렬되어 있다”, “파싱 (Parsing)과 직렬화 (Serialization)를 거쳐도 레코드가 보존된다”, “함수가 결정론적 (Deterministic)이다”, “결과값이 범위 내에 머문다” 등이 있습니다.
속성을 찾는 실질적인 방법은 다음과 같은 질문을 던지는 것입니다:
- 무엇이 항상 참이어야 하는가?
- 무엇이 절대 일어나서는 안 되는가?
- 작업을 반복한 후에도 무엇이 변하지 않고 유지되어야 하는가?
- 관련된 입력값들 사이에 어떤 관계가 성립해야 하는가?
이러한 질문들은 구현 (Implementation) 방식이 아닌 시스템 자체를 설명하는 테스트를 작성하도록 도와줍니다. 이는 대개 시간이 흐르며 코드가 변경되더라도 테스트가 더 탄력적으로(Resilient) 유지되게 만듭니다.
축소 (Shrinking) 및 디버깅 (Debugging)
Hypothesis의 가장 큰 강점 중 하나는 축소 (Shrinking) 기능입니다. Hypothesis는 실패하는 입력값을 찾으면, 여전히 실패를 유발하는 가장 작은 예시로 해당 입력을 줄이려고 시도합니다. 이를 통해 거대한 무작위 케이스보다 실패 원인을 더 쉽게 이해하고 더 빠르게 디버깅할 수 있습니다.
예를 들어, 긴 유니코드 (Unicode) 문자열이 포함된 복잡한 실패 사례가 실제 문제를 드러내는 단 한 글자의 문자열로 축소될 수 있습니다. 실제로 이는 읽기 어려운 테스트 케이스와 씨름하는 대신 버그 자체를 수정할 수 있도록 도와줍니다.
실무 워크플로우 (Practical workflow)
좋은 속성 기반 테스트 워크플로우는 다음과 같습니다:
- 하나의 작은 함수나 모듈로 시작합니다.
- 항상 유지되어야 하는 하나의 불변량 (Invariant)을 작성합니다.
- 동작의 다른 측면을 확인하기 위해 두 번째 속성을 추가합니다.
- 테스트를 실행하여 Hypothesis가 입력 공간 (Input space)을 탐색하게 합니다.
- 실패가 발생하면 코드를 최소화하고, 필요한 경우 속성을 강화합니다.
이 방식은 명확한 해피 패스 (Happy path)와 예상되는 에러 메시지를 확인하기 위한 몇 가지 예시 기반 테스트 (Example-based tests)와 결합했을 때 가장 효과적입니다. 속성 기반 테스트는 다른 모든 테스트를 대체하는 것이 아니라, 예시들이 자주 놓치는 까다로운 영역을 커버하는 방법입니다.
흔한 실수
흔한 실수는 “함수가 무언가를 반환한다” 또는 “함수가 충돌(Crash)하지 않는다”와 같이 너무 약한 속성을 작성하는 것입니다. 그러한 단언 (Assertion)들은 코드가 고장 난 상태에서도 통과하는 경우가 많습니다.
또 다른 실수는 속성 (Property)을 너무 구현 세부 사항에 종속적으로 (Implementation-specific) 만드는 것입니다. 이는 실제로 중요하게 생각하는 동작 대신 현재의 코드 구조에 얽매이게 만듭니다. 내부 구현 세부 사항이 아닌, 관찰 가능한 동작 (Observable behavior)을 목표로 삼으세요.
마지막으로, 입력 전략 (Input strategies)을 현실적으로 유지하세요. 만약 함수가 ID, 금액 또는 구조화된 레코드 (Structured records)를 기대한다면, 완전히 임의적인 데이터를 던지는 대신 해당 형태를 직접 생성하세요. 좋은 전략은 실패를 의미 있게 만들고 디버깅 속도를 높여줍니다.
완전한 예시
시작점으로 사용할 수 있는 간결한 테스트 스위트 (Test suite)입니다:
from hypothesis import given, strategies as st
from app import normalize_email, apply_discount
...
이 스위트는 정규화 규칙 (Normalization rule), 안정성 규칙 (Stability rule), 그리고 수치 경계 규칙 (Numeric boundary rule)을 검사합니다. 이들을 함께 사용하면 직접 작성한 소수의 예시들보다 훨씬 더 나은 커버리지 (Coverage)를 확보할 수 있습니다.
언제 사용해야 하는가
도메인에 입력 조합이 많을 때, 버그가 엣지 케이스 (Edge cases)에 숨어 있을 때, 또는 정확한 스냅샷 (Snapshots)보다 불변량 (Invariants)이 더 중요할 때 속성 기반 테스트를 사용하세요. 특히 파서 (Parsers), 직렬화 도구 (Serializers), 검증기 (Validators), 계산기 (Calculators), 데이터 변환 (Data transformations), 그리고 프로토콜 로직 (Protocol logic)에 매우 유용합니다.
반면, 몇 가지 예시만으로 출력을 예측하기가 매우 쉽거나, 주요 리스크가 논리적 정확성보다는 시각적 또는 워크플로 회귀 (Workflow regression)인 경우에는 유용성이 떨어집니다. 그러한 경우에는 예시 테스트 (Example tests), 승인 테스트 (Approval tests), 또는 탐색적 테스트 (Exploratory testing)가 더 적합할 수 있습니다.
다음 단계
기본적인 속성들에 익숙해졌다면, 여러 전략을 결합하거나, 커스텀 생성기 (Custom generators)를 추가하고, 상태 기반 동작 (Stateful behavior)을 테스트해 보세요. 다음으로 해볼 만한 좋은 연습은 파서/직렬화 도구 쌍을 테스트하거나, 리스트, 날짜, 또는 금액을 조작하는 함수를 테스트하는 것입니다.
길러야 할 핵심 습관은 간단합니다. "다음에 어떤 예시를 테스트해야 하지?"라고 묻는 것을 멈추고, "무엇이 항상 참이어야 하는가?"라고 묻기 시작하는 것입니다. 이러한 관점의 전환이 속성 기반 테스트를 매우 효과적으로 만드는 핵심입니다.
Rizwan Saleem | https://rizwansaleem.co
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기