테스트 자동화를 위한 Python: 실제로 중요한 부분들
요약
테스트 자동화 엔지니어에게 실질적으로 필요한 Python 학습 방향을 제시합니다. 일반적인 프로그래밍 강의와 달리, Playwright 및 pytest 활용에 최적화된 핵심 기술과 Python의 가독성이 테스트 유지보수에 주는 이점을 강조합니다.
핵심 포인트
- 테스트 자동화에 특화된 Python 학습의 중요성
- 가독성을 통한 테스트 코드의 문서화 기능
- pytest, Playwright 등 강력한 테스트 생태계 활용
- 불필요한 개념을 배제한 실무 중심의 학습 전략
테스트 자동화 엔지니어가 실제로 사용하는 Python에 대한 완전하고 정직한 가이드 — 그리고 안심하고 무시해도 되는 80%에 대하여.
테스터로서 Python을 학습할 때 발생하는 문제
여러분이 공감할 만한 시나리오를 하나 설명해 보겠습니다.
여러분은 테스트 자동화 분야로 전환하기로 결심합니다. 모두가 Python을 배우라고 말합니다. 그래서 여러분은 40시간 분량의, 평점이 높고 좋은 Python 강의를 찾아냅니다. 그리고 시작합니다.
1주 차: 변수 (variables), 루프 (loops), 함수 (functions). 괜찮습니다. 유용합니다. 2주 차: 숫자 맞추기 게임을 만듭니다. 3주 차: 짖는 Dog 클래스를 통한 객체 지향 프로그래밍 (object-oriented programming). 4주 차: matplotlib 차트. 5주 차: pandas 데이터프레임 (dataframes). 6주 차: Flask 웹 앱.
8주 차가 되었을 때, 여러분은 인생에서 그 어느 때보다 많은 코드를 작성했지만, 테스트 데이터 (test data)를 어떻게 구조화해야 하는지, 왜 자동화 테스트가 간헐적으로 계속 실패하는지, 혹은 픽스처 (fixture)가 무엇인지에 대해서는 여전히 전혀 알지 못합니다.
여러분은 잘못된 것을 배운 것이 아닙니다. 잘못된 순서로, 잘못된 목표를 향해, 올바른 것을 배운 것입니다.
데이터 과학을 위한 Python, 웹 개발을 위한 Python, 그리고 테스트 자동화를 위한 Python은 구문 (syntax)만 공유할 뿐 그 외에는 거의 아무것도 공유하지 않습니다. 테스터가 매일 사용하는 Python의 20%는 데이터 과학자가 사용하는 20%가 아닙니다. 일반적인 강의들은 폭넓은 지식을 가르칩니다. 하지만 테스트 자동화는 매우 특정한 깊이를 요구합니다.
그래서 이 글은 다른 방식을 취합니다. 테스트 자동화가 실제로 사용하는 Python을 다룹니다 — 모든 개념이 깨끗하고, 신뢰할 수 있으며, 유지보수가 가능한 자동화 테스트를 작성하는 직무를 정조준합니다. 숫자 맞추기 게임은 없습니다. 짖는 개도 없습니다. 오직 여러분을 Playwright 및 pytest와 함께 강력하게 만들어 줄 Python만 있습니다.
내용이 깁니다. 북마크해 두세요. 시작해 봅시다.
파트 1: 왜 Python이 테스트 자동화를 점령했는가
구문을 살펴보기 전에, "왜 Python인가?"라는 질문에 대한 빠르고 정직한 답변을 드리겠습니다. 왜 여러분의 도구가 승리했는지 그 이유를 아는 것이, 그 도구를 어떻게 사용하는지를 알려주기 때문입니다.
마치 의도가 읽히는 것처럼 느껴집니다. 이는 다른 어떤 분야보다 테스트에서 더 중요합니다. 테스트는 실행되는 문서 (documentation)입니다. 새벽 2시에 테스트가 실패했을 때, 누군가는 그 테스트를 읽고 무엇을 확인하려 했는지 즉시 이해해야 합니다. Python의 가독성 (readability)은 미적인 선호도가 아니라, 유지보수 기능 (maintenance feature)입니다.
비교해 보세요:
def test_user_can_checkout_with_valid_card(page):
login(page, user="test@example.com")
add_to_cart(page, product="Backpack")
...
여러분은 Python을 알기도 전에 저 코드가 무엇을 하는지 알았습니다. 그것이 바로 핵심입니다.
테스트를 위한 생태계는 타의 추종을 불허합니다. pytest는 아마도 어떤 언어를 통틀어 최고의 테스트 프레임워크 (test framework)일 것입니다. Playwright의 Python 바인딩 (bindings)은 일급 시민 (first-class) 수준입니다. requests, faker, pydantic, allure — 여러분에게 필요한 도구들은 이미 존재하며 성숙해 있습니다.
작성-실행 사이클 (write-run cycle)이 즉각적입니다. 컴파일 (compile) 단계가 없습니다. 한 줄을 바꾸고, 테스트를 실행하고, 결과를 확인합니다. 밤 11시에 불안정한 테스트 (flaky test)를 디버깅 (debugging)할 때, 이 루프 속도는 문제를 해결하느냐 포기하느냐의 차이를 만듭니다.
업계 표준입니다 (JavaScript와 함께). 이는 여러분의 기술이 직장을 옮겨도 그대로 적용되며, 문제에 대한 해답이 이미 Stack Overflow에 있고, 여러분이 원하는 모든 자동화 채용 공고에 Python이 나열되어 있음을 의미합니다.
상황은 이렇습니다. 이제 기술(craft)로 들어가 봅시다.
파트 2: 변수와 타입 — 테스터의 시각으로
모든 튜토리얼은 변수 (variables)로 시작합니다. 대부분은 잘못된 방식으로 시작합니다:
x = 5
y = "hello"
이것은 구문 (syntax)만 가르칠 뿐 그 외에는 아무것도 가르치지 않습니다. 여러분이 실제로 원하는 직업을 위해 동일한 개념을 가르쳐 보겠습니다:
BASE_URL = "https://shop.example.com"
TIMEOUT_MS = 30_000
HEADLESS = True
...
방금 어떤 일이 일어났는지 주목하세요. 동일한 네 가지 타입 (str, int, bool, str)이지만, 이제 여러분은 실질적인 것을 배웠습니다: 설정 (configuration)은 흩어진 매직 넘버 (magic values)가 아니라 이름이 지정된 상수 (named constants)에 존재합니다.
30_000의 _는 가독성을 위한 구분자 (readability separator)이며, Python은 이를 무시합니다. 타임아웃 (timeouts), 임계값 (thresholds), 그리고 0이 뭉쳐 보여 헷갈리는 모든 숫자에서 유용합니다.
중요한 타입들과 그 이유
| 타입 | 테스터가 접하게 되는 사례 |
|---|---|
str | URL, 셀렉터 (selectors), 예상 텍스트, 테스트 데이터, 파일 경로 |
| ... | |
| 그게 전부입니다. 이 리스트가 끝입니다. 복잡한 숫자는 필요하지 않을 것입니다. |
모든 초보자를 물어뜯는 None 함정
None은 "여기에 아무것도 없음"을 의미합니다. 이는 0도 아니고, ""도 아니며, False도 아닙니다. 그리고 이 차이점은 테스터들이 끊임없이 마주치는 특정한 버그를 유발합니다:
discount = get_discount_from_config() # 설정되지 않은 경우 None을 반환
if not discount: # ❌ 버그: discount가 0일 때도 True가 됨
...
0이라는 정당한 할인율은 첫 번째 버전에서 잘못된 기본값을 트리거하게 됩니다. 왜냐하면 0은 falsy(거짓 같은 값)이기 때문입니다. "설정되지 않음"을 의미할 때는 is None을 사용하세요. "비어 있거나, 0이거나, 누락된 상태 중 무엇이든 상관없음"을 의미할 때는 truthiness (참/거짓 판별)를 사용하세요.
이것은 실제로 배포되는 실질적인 버그입니다. 또한 전형적인 면접 질문이기도 합니다.
== vs is — 당신이 질문받게 될 내용
a = "hello"
b = "hello"
a == b # True — 동일한 값
...
테스터를 위한 규칙: 단언문 (assertions)에서는 모든 것에 ==를 사용하세요. is는 오직 None, True, False와 함께만 사용하세요. 만약 다른 무언가에 is를 사용하고 있다면, 아마도 ==를 사용하려 했던 것일 겁니다.
파트 3: 문자열 (Strings) — 당신이 주로 머물게 될 곳
테스터는 셀렉터 (selectors), 예상 메시지, URL, 테스트 데이터, 파일 경로, 로그 출력 등 문자열을 끊임없이 다룹니다. 능숙해지세요.
f-strings: 당신에게 필요한 유일한 포매팅 (formatting)
user_id = 42
env = "staging"
...
깔끔하고, 읽기 쉬우며, 빠릅니다. 그 외의 것들 (% 포매팅, .format())은 레거시 (legacy)입니다. 오래된 코드에서는 보게 되겠지만, 직접 작성해서는 안 됩니다.
디버깅을 위한 핵심 기능 — = 지정자 (specifier):
actual_count = 3
expected_count = 5
print(f"{actual_count=}, {expected_count=}")
...
이것은 변수 이름과 값을 모두 출력합니다. 새벽 1시에 테스트를 디버깅하고 있을 때, 이것은 정말 많은 시간을 아껴줍니다.
테스터가 실제로 사용하는 문자열 메서드 (string methods)
text = " Order Confirmed "
text.strip() # 'Order Confirmed' — 스크래핑된 텍스트의 공백 제거
...
테스트에서 .strip()이 매우 중요한 이유: 웹 페이지에서 스크래핑(scraping)한 텍스트에는 HTML 포맷팅으로 인한 보이지 않는 공백(whitespace)과 줄바꿈(newlines)이 포함되는 경우가 빈번합니다. 분명히 맞게 보이는 단언(assertion)이 실패하고, 이로 인해 20분을 허비하게 될 수도 있습니다:
actual = page.get_by_test_id("status").inner_text() # '\n Confirmed \n'
assert actual == "Confirmed" # ❌ 실패. 보이지 않는 공백 때문.
assert actual.strip() == "Confirmed" # ✅ 통과.
(더 좋은 방법은 Playwright의 expect(locator).to_have_text("Confirmed")를 사용하는 것입니다. 이는 공백을 정규화(normalise)할 뿐만 아니라 자동 재시도(auto-retries) 기능도 제공합니다. 하지만 가공되지 않은 문자열(raw strings)을 비교할 때는 .strip()이 여러분의 조력자가 될 것입니다.)
경로와 정규표현식(regex)을 위한 raw string
path = "C:\Users\test\new_file.txt" # ❌ \t와 \n이 탭과 줄바꿈으로 처리됨!
path = r"C:\Users\test\new_file.txt" # ✅ raw string — 백슬래시(\)가 문자 그대로 유지됨
...
r 접두사는 "백슬래시를 문자 그대로 처리하라"는 의미입니다. Windows 경로와 정규표현식(regex)을 다룰 때 타협할 수 없는 필수 사항입니다.
파트 4: 리스트(Lists)와 튜플(tuples) — 순서가 중요합니다
리스트(Lists): 가변적(mutable)이며, 가장 많이 쓰이는 도구
browsers = ["chromium", "firefox", "webkit"]
browsers.append("edge") # 끝에 추가
...
실제로 사용되는 경우: 테스트 데이터의 집합, 페이지에서 스크래핑한 요소(elements)의 리스트, 실행 대상 브라우저 목록, 정리해야 할 파일 목록 등입니다.
슬라이싱(Slicing) — 간편하고 유용함
items = ["a", "b", "c", "d", "e"]
items[:3] # ['a', 'b', 'c'] — 처음 세 개
...
실제 활용 사례: 마지막 5개의 테스트 실행 결과를 확인하기 위한 results[-5:], 또는 결과의 첫 페이지만 테스트하기 위한 products[:10] 등이 있습니다.
튜플(Tuples): 불변적(immutable)이며, 그것이 핵심입니다
email, password = credentials # 언패킹(unpacking) — 깔끔하고 가독성이 좋음
테스터가 주목해야 하는 이유: 튜플은 "이 값은 변하지 않는다"라고 선언하는 것과 같습니다. 이는 테스트 데이터에서 매우 강력한 신호가 됩니다. 튜플로 정의된 테스트 케이스는 하나의 테스트에 의해 실수로 변형(mutated)되어 다음 테스트를 망가뜨리는 일이 발생하지 않습니다.
이는 매개변수화된 테스트(parametrised testing)에서 매우 중요합니다:
CHECKOUT_CASES = [
("valid card", "4111111111111111", True),
("expired card", "4000000000000069", False),
...
각 케이스는 튜플 (tuple)입니다. 고정되어 있고, 안전합니다. 그리고 이 정확한 구조는 pytest의 parametrize로 바로 전달됩니다. 이것이 바로 데이터 주도 테스트 (data-driven testing)가 시작되는 지점입니다.
경험 법칙 (Rule of thumb): 만약 컬렉션이 고정된 기록(테스트 케이스, 좌표, 자격 증명 쌍 등)이라면 → 튜플 (tuple)을 사용하세요. 만약 컬렉션이 계속 늘어나는 구조라면 → 리스트 (list)를 사용하세요.
파트 5: 딕셔너리 (Dictionaries) — 테스터에게 가장 중요한 구조
만약 제가 여러분에게 테스트 자동화를 위해 단 하나의 데이터 구조를 마스터하라고 권한다면, 그것은 바로 딕셔너리 (dictionary)일 것입니다.
test_user = {
"email": "qa@example.com",
"password": "SecurePass123",
...
기본값을 사용하는 .get()은 테스터의 습관입니다. 테스트 데이터는 종종 불완전합니다. 설정 (config)에 모든 키 (key)가 없을 수도 있습니다. .get()을 사용한다는 것은 키가 누락되었을 때 프로그램이 폭발하는 대신 우아하게 성능을 저하시키며(degrade gracefully) 처리됨을 의미합니다.
딕셔너리가 테스트를 지배하는 이유
왜냐하면 현대적인 테스트의 모든 것은 딕셔너리이기 때문입니다:
# API 응답은 딕셔너리입니다
response = api.get("/users/42").json()
assert response["status"] == "active"
...
딕셔너리를 깊이 있게 배우면 API 테스트가 자연스러워집니다. 왜냐하면 .json()을 호출하는 순간 JSON 응답은 곧 Python 딕셔너리가 되기 때문입니다.
딕셔너리 반복 (Iterating)
for key, value in test_user.items():
print(f"{key}: {value}")
...
중첩 접근 (Nested access) — 그리고 안전한 방법
API 응답은 중첩됩니다. 아주 깊게 말이죠. 그리고 중첩된 접근은 테스트가 충돌(crash)하는 지점입니다:
data = {"user": {"profile": {"email": "qa@example.com"}}}
data["user"]["profile"]["email"] # 작동함
...
{} 기본값을 사용하는 체이닝된 .get() 방식은 절대 충돌하지 않습니다. 실제 API 응답을 다루는 테스트에서 이 패턴은 암기할 가치가 있습니다. 이는 치명적인 충돌을 우아한 기본값 처리로 바꿔줍니다.
파트 6: 세트 (Sets) — 작지만 진정으로 유용한
세트 (Sets)는 고유한 아이템들의 순서가 없는 컬렉션입니다. 테스터들은 이를 충분히 활용하지 못하고 있습니다.
expected_ids = {"user-1", "user-2", "user-3"}
actual_ids = {"user-1", "user-3", "user-4"}
...
결정적인 유스케이스 (Killer use case): 순서가 중요하지 않은 컬렉션 (collections)을 비교하는 것입니다. API가 반환하는 아이템 리스트는 어떤 순서로든 돌아올 수 있습니다. 리스트 (list)를 비교하면 순서 때문에 실패하게 됩니다. 세트 (set)를 비교하면 당신이 실제로 신경 쓰는 부분인 _내용물 (contents)_을 테스트할 수 있습니다.
# ❌ 취약함 — API가 순서를 바꾸면 실패함
assert response["tags"] == ["python", "testing", "automation"]
...
이 한 줄의 코드가 잘못된 실패 (false failures)라는 카테고리 전체를 제거해 줍니다.
또한: set(items)는 즉시 중복을 제거합니다. len(set(emails)) == len(emails)는 한 줄로 중복 여부를 확인합니다.
파트 7: 제어 흐름 (Control flow) — 작은 범위, 큰 영향력
조건문 (Conditionals)
if response.status == 200:
validate_success(response)
elif response.status == 429:
...
간단합니다. 하지만 테스트와 관련된 한 가지 주의사항이 있습니다: 테스트 내부에서 조건문을 사용하는 것은 매우 주의해야 합니다.
def test_checkout(page):
if page.get_by_text("Sale banner").is_visible(): # ⚠️ 위험
apply_discount(page)
...
이 테스트는 이제 실행할 때마다 서로 다른 동작을 수행합니다. 즉, 테스트가 실패했을 때 어떤 경로를 거쳤는지 알 수 없다는 뜻입니다. 테스트 내부의 조건부 로직은 비결정론적 테스트 (non-deterministic tests)를 만드는데, 이는 당신이 제거하려고 노력하는 바로 그 대상입니다.
규칙: 조건문은 헬퍼 (helpers), 픽스처 (fixtures), 그리고 페이지 오브젝트 (page objects)에 있어야 합니다. 테스트 자체는 직선 형태여야 합니다. 만약 진정으로 두 개의 경로가 필요하다면, 두 개의 테스트를 작성하세요.
루프 (Loops)
for browser in ["chromium", "firefox", "webkit"]:
run_suite(browser)
...
enumerate와 zip은 테스터들이 가장 많이 사용하는 두 가지 루프 헬퍼입니다. 위치 정보가 필요할 때는 enumerate를, 서로 연관된 두 리스트를 함께 순회할 때는 zip을 사용합니다.
테스트에서의 루프 안티 패턴 (The loop anti-pattern in tests)
def test_all_products(page):
for product in ALL_100_PRODUCTS: # ❌ 하나의 테스트, 100개의 체크
verify_product_page(page, product)
만약 47번 제품이 실패하면 테스트는 중단됩니다. 48~100번 제품에 대해서는 알 수 없게 됩니다. 그리고 보고서에는 "100개 중 1개의 제품이 고장 남" 대신 "1개의 테스트 실패"라고 표시됩니다.
해결책은 매개변수화 (parametrisation, pytest의 @pytest.mark.parametrize)입니다. 이를 통해 하나의 테스트를 100개의 독립적인 테스트로 전환할 수 있습니다. 이는 Volume 12의 영역이지만, Python의 직관은 여기서부터 시작됩니다: 테스트 내부의 루프는 보통 매개변수화된 테스트 (parametrised test)가 되어야 합니다.
Part 8: 함수(Functions) — 테스트 코드가 유지보수 가능해지는 지점
def login(page, email: str, password: str) -> None:
page.goto("/login")
page.get_by_label("Email").fill(email)
...
함수는 반복되는 테스트 단계가 더 이상 반복되지 않게 만드는 방법입니다. 이것이 페이지 객체 모델 (Page Object Model)의 씨앗입니다. 즉, 이 아이디어를 클래스(classes)로 조직화한 것뿐입니다.
기본 인자 (Default arguments) — 그리고 가변 기본값의 함정 (mutable default trap)
def create_user(name, role="viewer"): # ✅ 합리적인 기본값
...
...
두 번째 사례는 진짜 Python의 지뢰입니다. 기본값인 []는 함수가 호출될 때마다 생성되는 것이 아니라, 함수가 정의될 때 단 한 번 생성됩니다. 따라서 다음과 같은 결과가 나타납니다:
add_test_data("a") # ['a']
add_test_data("b") # ['a', 'b'] ← 리스트가 유지되었습니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기