Selenium × pytest 실천 가이드|fixture・parametrize・conftest.py・mark를 실무 수준으로 해설

테스트 자동화

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 전체 구성이란?

폴더 구성

먼저 전체 폴더 구성과 각 파일의 역할을 파악합니다.

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.pyWebDriver의 기동・종료 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 이미지에서 고정 버전을 사용하는 구성도 일반적입니다
📖 WebDriverWait(명시적 대기)의 사용법을 더 자세히 알고 싶은 분은 이쪽Selenium×Python 환경 구축 완전 가이드

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)로 분리하면 테스트 로직과 데이터를 분리할 수 있습니다. 사양 변경 시 데이터만 추가・수정하면 되므로 유지보수성이 크게 올라갑니다.
📖 pytest의 parametrize・fixture를 더 깊이 배우고 싶은 분은 이쪽Python pytest 완전 가이드

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"
📖 GitHub Actions에서 mark를 활용한 CI/CD 자동 실행GitHub Actions × Playwright로 E2E 테스트를 자동화하는 방법

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")
📖 테스트 코드를 더 정리하는 「Page Object Model」패턴Page Object Model이란?Playwright × pytest로의 구현 방법

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 -spytest.ini에 추가합시다.

⑥ assert 전 예외로 ERROR가 된다

요소를 찾지 못하는 등 assert보다 전에 예외가 발생하면 FAILED가 아닌 ERROR가 됩니다. WebDriverWait로 요소의 존재를 확인하고 나서 assert합시다.

⑦ 프로젝트 루트 이외에서 실행하면 ModuleNotFoundError

# NG
cd tests && pytest

# OK:프로젝트 루트에서 실행
cd selenium-project
pytest

FAQ

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"은 무엇을 실행하나요?

smokeregression 양쪽의 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를 사용하여 기존 테스트의 데이터를 분리하는 것부터 시작해 보세요. 그 하나의 패턴을 구현하는 것만으로 「테스트 설계」와 「테스트 구현」이 분리되어 테스트 스위트 전체의 가독성이 한 번에 좋아집니다.

제목과 URL을 복사했습니다