Selenium × pytest에서 자주 사용하는 4가지 기능——fixture・parametrize・conftest.py・mark——을 실무 수준의 샘플 코드와 함께 해설합니다.
pytest의 fixture・parametrize・mark를 익히면 Selenium 테스트를 「단발 스크립트」에서 「유지보수하기 쉬운 테스트 스위트」로 개선할 수 있습니다.
이 글에서는 실무에서 바로 사용할 수 있는 구성을 코드와 함께 소개합니다.
이 글에서 해설하는 4가지 포인트
- pytest fixture:WebDriver의 셋업・티어다운을 공통화한다
- pytest parametrize:하나의 테스트를 여러 데이터로 반복하는 데이터 기반 테스트
- conftest.py:fixture를 모든 테스트 파일에서 공유하기 위한 설정 파일
- pytest mark:smoke・regression 등 목적별로 테스트를 분류・선택 실행한다
👉 전제:Selenium × Python 환경 구축이 완료되어 있는 것을 전제로 합니다. 아직인 분은 환경 구축 가이드를 먼저 확인하세요.
💡 다른 글과의 차이
많은 글은 「테스트가 동작한다」는 곳에서 끝나지만 이 글에서는 실무에서 사용할 수 있는 설계 패턴(fixture 분리・parametrize에 의한 데이터 기반・mark에 의한 테스트 분류)까지 깊이 해설합니다.
Selenium 테스트에는 「일단 동작한다」에서 「팀에서 사용할 수 있고 CI에서 돌릴 수 있는」 수준까지의 벽이 있습니다. 그 벽을 넘는 것이 pytest의 fixture・parametrize・mark를 익히는 것입니다.
📌 이런 분께 추천합니다 / 읽으면 얻을 수 있는 것
- Selenium × pytest 환경 구축이 완료되어 테스트를 작성하기 시작한 분
- fixture・parametrize・conftest.py・mark의 사용법을 체계적으로 학습하고 싶은 분
- 테스트 코드를 정리하여 CI/CD에서 사용할 수 있는 수준으로 만들고 싶은 분
- → pytest fixture・parametrize・mark의 실천적인 사용법을 알 수 있다
- → conftest.py를 사용한 WebDriver 공통화 패턴을 알 수 있다
- → 로그인 폼의 정상 계통・이상 계통 테스트를 구현할 수 있다
👤 이 글을 쓴 사람
QA 엔지니어・테스트 자동화 엔지니어로서 15년 이상의 실무 경험을 가진 Yoshi가 집필. Selenium × pytest의 구성은 실무 프로젝트에서 매일 사용하고 있으며 fixture・parametrize・mark의 패턴을 팀의 표준으로 전개하고 있습니다. 구현 코드는 GitHub에 공개 중입니다: github.com/YOSHITSUGU728/automated-testing-portfolio
- Selenium × pytest 전체 구성이란?
- STEP 1:pytest fixture란?conftest.py로 Selenium WebDriver를 공통화하는 방법
- STEP 2:Selenium 테스트 작성법(정상 계통・이상 계통)
- STEP 3:pytest parametrize에 의한 데이터 기반 테스트 구현
- STEP 4:pytest mark로 테스트를 분류・선택 실행한다
- STEP 5:fixture의 응용(셋업・티어다운)
- STEP 6:실천적인 테스트 예시(회원 가입 폼)
- ⚠️ Selenium × pytest에서 자주 있는 실수 7선
- FAQ
- 📋 이 글의 정리
Selenium × pytest 전체 구성이란?
폴더 구성
먼저 전체 폴더 구성과 각 파일의 역할을 파악합니다.
selenium-project/
├── venv/
├── tests/
│ ├── conftest.py # WebDriver fixture・공통 설정
│ ├── test_login.py # 로그인 테스트
│ ├── test_search.py # 검색 테스트
│ └── test_register.py # 회원 가입 테스트
├── pages/
│ └── login_page.py # Page Object(별도 글에서 해설합니다)
├── requirements.txt
└── pytest.ini각 파일의 역할
📋 각 파일의 역할
| conftest.py | WebDriver의 기동・종료 fixture를 정의. 모든 테스트 파일에서 자동으로 읽힌다 |
| test_*.py | 기능별로 테스트를 분할. 1파일=1유스케이스가 이상적 |
| pytest.ini | 테스트 패스・옵션・커스텀 mark 등록 |
| pages/ | Page Object 패턴(요소 조작을 클래스에 집약) |
STEP 1:pytest fixture란?conftest.py로 Selenium WebDriver를 공통화하는 방법
conftest.py에 WebDriver의 fixture를 정의함으로써 모든 테스트 파일에서 공유할 수 있습니다.
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
@pytest.fixture(scope="function")
def driver():
"""테스트마다 Chrome을 기동・종료하는 fixture"""
options = webdriver.ChromeOptions()
# options.add_argument("--headless") # CI/CD 환경에서는 ON으로 한다
options.add_argument("--no-sandbox") # Linux/CI 환경용
options.add_argument("--disable-dev-shm-usage") # Linux/CI 환경용
options.add_argument("--window-size=1920,1080")
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(10) # 요소 검색 시 기본 대기(보조적 용도)
yield driver
try:
driver.quit()
except Exception:
# 세션 절단 시에도 teardown이 실패하지 않도록 보호
pass
@pytest.fixture(scope="function")
def logged_in_driver(driver):
"""로그인 상태가 필요한 테스트 전용 fixture.
로그인 불필요한 테스트에는 driver fixture를 직접 사용할 것."""
# ※ example-app.com은 샘플 URL입니다. 실제 테스트 대상 URL로 변경하세요.
driver.get("https://example-app.com/login")
driver.find_element(By.ID, "username").send_keys("test_user")
driver.find_element(By.ID, "password").send_keys("test_pass")
driver.find_element(By.ID, "login-btn").click()
# 로그인 완료를 대기(이것이 없으면 CI 환경에서 불안정해진다)
WebDriverWait(driver, 10).until(
EC.url_contains("/dashboard")
)
yield driver
# teardown(driver.quit())은 driver fixture 측에서 수행하므로 여기서는 불필요💡 logged_in_driver 사용 예시
# 로그인 상태가 필요한 테스트에만 logged_in_driver를 사용
def test_dashboard_title(logged_in_driver):
assert "대시보드" in logged_in_driver.title
# 로그인 불필요한 테스트는 driver를 직접 사용
def test_top_page_title(driver):
driver.get("https://example-app.com")
assert "홈" in driver.title💡 fixture의 포인트
scope="function"(기본):테스트마다 브라우저를 기동・종료. 안전하지만 느리다scope="session":테스트 스위트 전체에서 하나의 브라우저를 재사용. 빠르지만 상태 오염에 주의implicitly_wait(10):전체 요소 검색에 적용되는 기본 대기. WebDriverWait와의 병용이 완전히 금지된 것은 아니지만 양쪽 설정 시 대기 시간 예측이 어려워질 수 있습니다. WebDriverWait를 주축으로 하고 implicitly_wait는 최소한으로 하는 구성이 일반적입니다
⚠️ CI 환경에서의 주의점
scope="session"을 pytest-xdist(병렬 실행)와 조합하는 경우 세션 공유에 추가 설정이 필요합니다ChromeDriverManager().install()은 CI 기동 시마다 버전 확인이 실행되어 느려지는 경우가 있습니다. 대규모 CI에서는 Docker 이미지에서 고정 버전을 사용하는 구성도 일반적입니다
STEP 2:Selenium 테스트 작성법(정상 계통・이상 계통)
base_url fixture를 인수로 받음으로써 URL을 한 곳에서 관리할 수 있습니다. 하드코딩 없이 환경별 전환도 fixture 측에서만 대응할 수 있게 됩니다.
# tests/test_login.py
# ※ 이후 코드에서도 동일한 import를 사용합니다
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# ── 정상 계통 ──────────────────────────────────────
def test_login_success(driver, base_url):
"""정상 계통:올바른 ID・PW로 로그인 성공한다"""
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "username").send_keys("valid_user")
driver.find_element(By.ID, "password").send_keys("valid_pass")
driver.find_element(By.ID, "login-btn").click()
WebDriverWait(driver, 10).until(EC.url_contains("/dashboard"))
assert "/dashboard" in driver.current_url
# ── 이상 계통 ──────────────────────────────────────
def test_login_failure_wrong_password(driver, base_url):
"""이상 계통:패스워드가 틀린 경우 에러 메시지가 표시된다"""
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "username").send_keys("valid_user")
driver.find_element(By.ID, "password").send_keys("wrong_pass")
driver.find_element(By.ID, "login-btn").click()
error_msg = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, "[data-testid='login-error']") # data-testid 권장
)
)
assert "비밀번호가 올바르지 않습니다" in error_msg.text
def test_login_failure_empty_fields(driver, base_url):
"""이상 계통:ID와 PW가 빈 상태로 전송하면 유효성 검사 에러가 나온다"""
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "login-btn").click()
error = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CLASS_NAME, "validation-error"))
)
assert error.is_displayed()STEP 3:pytest parametrize에 의한 데이터 기반 테스트 구현
@pytest.mark.parametrize를 사용하면 동일한 테스트 로직을 여러 데이터로 반복 실행할 수 있습니다. 이상 계통의 패턴이 많은 경우에 특히 효과적입니다.
# tests/test_login.py(계속)
import pytest
# 테스트 데이터를 리스트로 정의(로직과 데이터를 분리)
INVALID_LOGIN_CASES = [
("", "valid_pass", "사용자명을 입력해 주세요"),
("valid_user", "", "패스워드를 입력해 주세요"),
("wrong_user", "valid_pass", "인증에 실패했습니다"),
("valid_user", "wrong_pass", "비밀번호가 올바르지 않습니다"),
("a" * 256, "valid_pass", "입력값이 너무 깁니다"),
]
@pytest.mark.parametrize("username, password, expected_error", INVALID_LOGIN_CASES)
def test_login_validation(driver, base_url, username, password, expected_error):
"""parametrize:여러 이상 계통 패턴을 정리하여 테스트"""
driver.get(f"{base_url}/login")
driver.find_element(By.ID, "username").send_keys(username)
driver.find_element(By.ID, "password").send_keys(password)
driver.find_element(By.ID, "login-btn").click()
error_msg = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CLASS_NAME, "error-message"))
)
assert expected_error in error_msg.text실행 결과 샘플
pytest tests/test_login.py -v
collected 8 items
test_login.py::test_login_success PASSED
test_login.py::test_login_failure_wrong_password PASSED
test_login.py::test_login_failure_empty_fields PASSED
test_login.py::test_login_validation[-valid_pass-사용자명을 입력해…] PASSED
test_login.py::test_login_validation[valid_user--패스워드를 입력해…] PASSED
test_login.py::test_login_validation[wrong_user-valid_pass-인증에…] PASSED
test_login.py::test_login_validation[valid_user-wrong_pass-비밀번호…] PASSED
test_login.py::test_login_validation[aaaa…-valid_pass-입력값이…] PASSED
========================== 8 passed in 18.34s ==========================INVALID_LOGIN_CASES)로 분리하면 테스트 로직과 데이터를 분리할 수 있습니다. 사양 변경 시 데이터만 추가・수정하면 되므로 유지보수성이 크게 올라갑니다.STEP 4:pytest mark로 테스트를 분류・선택 실행한다
@pytest.mark를 사용하면 테스트에 태그를 붙여 특정 그룹만 선택하여 실행할 수 있습니다. CI/CD에서 「스모크 테스트만 실행」「전체 테스트를 실행」하고 싶을 때 필수입니다.
pytest.ini에 커스텀 mark 등록
[pytest]
testpaths = tests
addopts = -v
markers =
smoke: 스모크 테스트(최우선・매회 실행)
regression: 리그레션 테스트(전체 확인 시 실행)
slow: 실행에 시간이 걸리는 테스트mark를 붙이고 선택 실행
@pytest.mark.smoke
def test_login_success(driver, base_url): ...
@pytest.mark.regression
def test_login_failure_wrong_password(driver, base_url): ...
@pytest.mark.slow
@pytest.mark.regression
def test_login_session_timeout(driver, base_url): ...
# 스모크 테스트만 실행(배포 후 확인에)
pytest -m smoke
# 느린 테스트를 제외하고 실행
pytest -m "not slow"
# smoke이면서 regression(양쪽 mark가 붙은 테스트만)
pytest -m "smoke and regression"STEP 5:fixture의 응용(셋업・티어다운)
# tests/conftest.py(추가)
import pytest
@pytest.fixture(scope="function")
def go_to_login(driver):
driver.get("https://example-app.com/login")
yield driver
import os, re, time
os.makedirs("screenshots", exist_ok=True)
# ※ 실무에서는 pytest_runtest_makereport로 실패 시에만 저장하는 구성이 일반적입니다
driver.save_screenshot(f"screenshots/{int(time.time())}.png")
@pytest.fixture(scope="session")
def base_url():
"""base_url을 fixture화(환경별 전환이 쉬워진다)
※ 이것으로 BASE_URL 상수는 불필요. 모든 테스트에서 이 fixture를 사용할 것."""
return "https://example-app.com" # ※ 샘플 URL입니다. 실제 URL로 변경하세요.💡 실패 시에만 스크린샷을 저장한다(CI 권장 구성)
매번 저장하면 CI에서 스토리지가 비대해집니다. pytest hook(pytest의 실행 타이밍에 독자적인 처리를 삽입할 수 있는 확장 기능)의 pytest_runtest_makereport를 사용하면 실패한 테스트만 저장할 수 있습니다.
import os, re, pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get("driver")
if driver:
os.makedirs("screenshots", exist_ok=True)
# Windows 금지 문자를 일괄 제거(\ / * ? : " < > |)
safe_name = re.sub(r'[\\/*?:"<>|]', "_", item.name)
driver.save_screenshot(f"screenshots/{safe_name}.png")STEP 6:실천적인 테스트 예시(회원 가입 폼)
# tests/test_register.py
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# ※ 실제 프로젝트에서는 같은 이메일 주소를 반복하면 중복 등록 에러가 됩니다
# 테스트마다 유니크화하는 경우:f"test_{int(time.time())}@example.com"
VALID_USER = {
"name": "테스트 사용자",
"email": "test@example.com",
"password": "SecurePass123!",
}
VALIDATION_CASES = [
("name", "", "이름을 입력해 주세요"),
("email", "not-an-email", "올바른 이메일 주소를 입력해 주세요"),
("email", "", "이메일 주소를 입력해 주세요"),
("password", "short", "패스워드는 8자 이상으로 입력해 주세요"),
]
@pytest.fixture
def register_page(driver, base_url):
"""회원 가입 페이지로 이동하는 fixture(base_url fixture로 URL을 통일)"""
driver.get(f"{base_url}/register")
yield driver
@pytest.mark.smoke
def test_register_success(register_page):
"""스모크:정상적인 회원 가입이 완료된다"""
d = register_page
d.find_element(By.ID, "name").send_keys(VALID_USER["name"])
d.find_element(By.ID, "email").send_keys(VALID_USER["email"])
d.find_element(By.ID, "password").send_keys(VALID_USER["password"])
d.find_element(By.ID, "submit-btn").click()
success = WebDriverWait(d, 10).until(
EC.visibility_of_element_located((By.CLASS_NAME, "success-message"))
)
assert "등록이 완료되었습니다" in success.text
@pytest.mark.regression
@pytest.mark.parametrize("field, value, expected_error", VALIDATION_CASES)
def test_register_validation(register_page, field, value, expected_error):
"""리그레션:유효성 검사 에러의 패턴 테스트"""
d = register_page
d.find_element(By.ID, "name").send_keys(VALID_USER["name"])
d.find_element(By.ID, "email").send_keys(VALID_USER["email"])
d.find_element(By.ID, "password").send_keys(VALID_USER["password"])
target = d.find_element(By.ID, field)
target.clear()
target.send_keys(value)
d.find_element(By.ID, "submit-btn").click()
error = WebDriverWait(d, 10).until(
# By.ID로 필드별 에러 요소를 특정(By.CLASS_NAME보다 정밀도가 높다)
EC.visibility_of_element_located((By.ID, f"{field}-error"))
)
assert expected_error in error.text📋 여기까지의 정리
- conftest.py:WebDriver의 기동・종료를 fixture에 집약 → 모든 테스트에서 공유
- parametrize:테스트 데이터를 리스트로 관리 → 이상 계통 패턴을 효율적으로 망라
- mark:smoke / regression으로 분류 → CI/CD에서 필요한 테스트만 선택 실행
- fixture 체인:driver → go_to_login → 테스트라는 처리 흐름을 명시
⚠️ Selenium × pytest에서 자주 있는 실수 7선
① test_ 프리픽스를 잊는다
함수명이 test_로 시작하지 않으면 pytest에 수집되지 않습니다. collected 0 items가 나오면 확인합시다.
② conftest.py의 위치를 잘못 놓는다
conftest.py는 참조하고 싶은 테스트 파일과 동일한 디렉토리 또는 그 상위에 놓아야 합니다.
tests/
├── conftest.py ← 여기에 놓는다(tests/ 바로 아래)
└── test_login.py③ scope=”session”으로 상태가 이어진다
scope="session"을 사용하면 테스트 간에 브라우저 상태(Cookie・세션)가 이어집니다. 테스트 순서에 의존하는 버그가 생기기 쉬우므로 scope="function"부터 시작합시다.
④ 커스텀 mark를 pytest.ini에 미등록으로 경고가 나온다
@pytest.mark.smoke 등의 커스텀 mark는 pytest.ini에 등록하지 않으면 PytestUnknownMarkWarning이 나옵니다.
[pytest]
markers =
smoke: 스모크 테스트
regression: 리그레션 테스트⑤ print()가 -s 없이 표시되지 않는다
기본적으로 print()의 출력이 캡처되어 표시되지 않습니다. 디버그 중에는 pytest -s로 실행하거나 addopts = -v -s를 pytest.ini에 추가합시다.
⑥ assert 전 예외로 ERROR가 된다
요소를 찾지 못하는 등 assert보다 전에 예외가 발생하면 FAILED가 아닌 ERROR가 됩니다. WebDriverWait로 요소의 존재를 확인하고 나서 assert합시다.
⑦ 프로젝트 루트 이외에서 실행하면 ModuleNotFoundError
# NG
cd tests && pytest
# OK:프로젝트 루트에서 실행
cd selenium-project
pytestFAQ
Q. fixture의 scope는 어느 것을 사용하면 좋을까요?
처음에는 scope="function"(기본값)을 권장합니다. 테스트마다 브라우저가 기동・종료되므로 상태 오염이 일어나기 어렵고 안전합니다. 테스트가 늘어 속도가 문제가 되면 scope="session"으로의 변경을 검토합시다. 단 session 스코프는 Cookie나 로그인 상태가 이어지는 점과 pytest-xdist(병렬 실행)와의 조합에는 추가 설정이 필요한 점에 주의가 필요합니다.
Q. implicitly_wait와 WebDriverWait는 어떻게 구분하여 사용하나요?
implicitly_wait는 전체 요소 검색에 전역으로 적용되는 대기입니다. WebDriverWait는 「표시되고 있는가」「클릭 가능한가」 등 특정 조건을 지정할 수 있는 명시적인 대기입니다. 완전히 병용 금지는 아니지만 양쪽 설정 시 대기 시간의 합산으로 예기치 않은 지연이 발생하는 경우가 있습니다. WebDriverWait를 주축으로 하고 implicitly_wait는 최소한의 값으로 설정하는 것이 표준입니다.
Q. parametrize의 데이터는 파일에서 읽어올 수 있나요?
할 수 있습니다. CSV나 JSON 파일에서 데이터를 읽어와 parametrize에 전달하는 방법이 자주 사용됩니다. json.load()나 csv.reader()로 리스트를 만들어 @pytest.mark.parametrize에 전달하는 방법이 일반적입니다.
Q. pytest -m "smoke and regression"은 무엇을 실행하나요?
smoke와 regression 양쪽의 mark가 붙어 있는 테스트만 실행됩니다. 「smoke 또는 regression 중 하나가 붙어 있는 테스트」를 실행하고 싶은 경우는 pytest -m "smoke or regression"을 사용합니다. 초보자가 헷갈리기 쉬운 포인트입니다.
Q. By.CLASS_NAME으로 에러 요소를 취득할 때 불안정해지는 경우가 있나요?
있습니다. 동일한 클래스명의 요소가 복수 존재할 때 의도하지 않은 요소를 취득해 버립니다. 이 글에서는 By.ID, f"{field}-error"와 같이 필드별 ID를 사용하는 방법을 채택하고 있습니다. 더 안정시키고 싶은 경우는 data-testid 속성을 사용한 로케이터(By.CSS_SELECTOR, "[data-testid='name-error']")가 권장됩니다.
Q. 테스트에서 같은 이메일 주소를 사용하면 중복 등록 에러가 되나요?
됩니다. 이 글의 VALID_USER는 설명을 위해 이메일 주소를 고정하고 있지만 실제 프로젝트에서는 2회째 이후에 「이미 등록되어 있는 이메일 주소입니다」라는 에러가 됩니다. 테스트마다 유니크한 이메일을 생성하는 경우는 f"test_{int(time.time())}@example.com"과 같이 타임스탬프를 사용하는 방법이 일반적입니다.
Q. fixture와 setUp/tearDown(unittest 형식)의 차이는?
setUp/tearDown은 클래스 기반의 테스트에 내장된 초기화・종료 처리입니다. pytest의 fixture는 함수 기반으로 사용할 수 있고 scope로 공유 범위를 유연하게 제어할 수 있다는 것이 큰 차이입니다. 또한 fixture는 인수로 주입하므로 재사용・조합이 쉽습니다. pytest 프로젝트에서는 fixture가 표준입니다.
Q. conftest.py는 여러 개 만들 수 있나요?
만들 수 있습니다. pytest는 디렉토리 계층에 따라 conftest.py를 자동으로 읽어들입니다. 프로젝트 루트의 conftest.py에 전체 공통 fixture를, 서브디렉토리의 conftest.py에 그 디렉토리 전용 fixture를 정의하는 패턴이 일반적입니다.
Q. Selenium 테스트는 Page Object로 분할해야 하나요?
테스트가 10〜20개를 넘어오면 분할을 검토합시다. Page Object Model(POM)을 사용하면 로케이터 정의와 조작 로직을 페이지 클래스에 집약할 수 있어 요소 변경이 클래스 1곳의 수정으로 끝납니다. 처음부터 만들 필요는 없으며 중복 코드가 늘어오면 도입하는 것이 현실적입니다.
Q. pytest-xdist로 병렬 실행할 수 있나요?
할 수 있습니다. pip install pytest-xdist하고 pytest -n auto로 실행하면 CPU 코어 수에 따라 병렬 실행됩니다. 단 scope="session"의 fixture는 병렬 실행 시 브라우저를 공유할 수 없으므로 각 워커에서 독립된 fixture를 사용하는 설계가 필요합니다.
Q. CI/CD에서는 headless 모드가 필수인가요?
디스플레이가 없는 CI 환경(GitHub Actions・Jenkins 등)에서는 --headless가 필수입니다. 로컬 개발 시에는 끄면 브라우저의 동작을 눈으로 확인할 수 있어 디버그에 편리합니다. conftest.py의 --headless 줄을 코멘트 아웃하고 CI에서는 환경 변수로 전환하는 구성이 사용하기 쉽습니다.
Q. fixture의 autouse란 무엇인가요?
@pytest.fixture(autouse=True)를 붙이면 테스트 함수의 인수에 쓰지 않아도 자동으로 적용됩니다. 예를 들어 「모든 테스트에서 브라우저를 최대화하고 싶다」와 같은 횡단적인 처리에 사용합니다. 단 의도하지 않은 테스트에도 적용되므로 필요한 범위를 scope와 conftest.py의 배치로 제어합시다.
Q. fixture의 yield와 return의 차이는?
return은 테스트 전의 처리(셋업)만 쓸 수 있습니다. yield를 사용하면 yield 앞이 셋업・뒤가 티어다운(후처리)이 됩니다. WebDriver처럼 「기동 → 테스트 → 종료」라는 전후의 처리가 필요한 경우는 yield가 유일한 선택입니다. return은 간단한 값을 반환하는 fixture(base_url 등)에 사용합니다.
Q. Selenium과 Playwright 중 어느 것을 배워야 하나요?
먼저 Selenium을 배울 것을 권장합니다. 이유는 구인・Upwork 프로젝트 수가 압도적으로 많기 때문입니다. Selenium의 기초(WebDriver・대기 처리・pytest 연계)를 습득하고 나서 Playwright로 이행하면 개념의 차이를 이해하기 쉬워집니다. 양쪽 모두 할 수 있으면 엔지니어로서의 시장 가치가 크게 올라갑니다.
Q. pytest.ini와 pyproject.toml의 차이는?
pytest.ini는 pytest 전용의 설정 파일로 심플합니다. pyproject.toml은 Python 프로젝트 전체(의존 관계・빌드・린터 등)를 일원 관리할 수 있는 모던한 형식으로 [tool.pytest.ini_options] 섹션에 동일한 설정을 쓸 수 있습니다. 신규 프로젝트는 pyproject.toml 권장이지만 기존 프로젝트는 pytest.ini로 문제 없습니다.
📖 관련 글
실무에서는 fixture・parametrize・mark의 패턴을 팀의 표준으로 정의하여 신규 멤버가 참가했을 때 동일한 구성으로 테스트를 쓸 수 있도록 하고 있습니다. 특히 「테스트 데이터를 리스트로 분리하여 parametrize에 전달」하는 패턴은 사양 변경 시에 로직이 아닌 데이터만 수정하면 되므로 유지보수 비용이 크게 낮아집니다.
이 글의 구성대로 진행하면 테스트 설계와 테스트 구현이 명확하게 분리된 유지보수하기 쉬운 테스트 스위트가 완성됩니다.
📋 이 글의 정리
- conftest.py에 WebDriver fixture를 정의함으로써 모든 테스트 파일에서 공유할 수 있다
- parametrize로 테스트 데이터와 로직을 분리하고 이상 계통 패턴을 효율적으로 망라할 수 있다
- mark로 테스트를 분류하여 「스모크만」「리그레션만」하고 선택 실행할 수 있다
- fixture 체인으로 셋업・티어다운 처리를 일원 관리할 수 있다
- 실수 포인트(
test_망각・conftest 위치・scope의 인계)를 사전에 알아두면 막히지 않는다
먼저 parametrize를 사용하여 기존 테스트의 데이터를 분리하는 것부터 시작해 보세요. 그 하나의 패턴을 구현하는 것만으로 「테스트 설계」와 「테스트 구현」이 분리되어 테스트 스위트 전체의 가독성이 한 번에 좋아집니다.

