pytest는 Python에서 가장 널리 사용되는 테스트 프레임워크로, 심플한 문법과 강력한 fixture 기능으로 QA 엔지니어의 테스트 자동화를 극적으로 효율화합니다.
📌 이런 분께 추천합니다
- Python으로 테스트 자동화를 시작하고 싶은 QA 엔지니어·개발자
- pytest의 기초부터 fixture·parametrize까지 체계적으로 배우고 싶은 분
- unittest에서 pytest로 마이그레이션을 검토 중인 분
- Playwright나 Selenium과 조합하여 테스트를 관리하고 싶은 분
✅ 이 글을 읽으면 얻을 수 있는 것
- pytest 설치부터 첫 테스트 실행까지 막힘 없이 진행할 수 있다
- fixture·parametrize·conftest.py를 실전 수준으로 활용할 수 있다
- 실무 수준의 테스트 구성·리포트 출력까지 한 번에 이해할 수 있다
👤 이 글을 쓴 사람
QA 엔지니어로서 실무에서 Selenium·Playwright·pytest를 활용한 테스트 자동화에 종사 중입니다. 실제 프로젝트에서 작성한 코드를 바탕으로, 현장에서 진짜 도움이 되는 지식만을 엄선하여 해설합니다. 구현 코드는 GitHub에 공개 중입니다: github.com/YOSHITSUGU728/automated-testing-portfolio
📌 결론:pytest에서 꼭 알아야 할 3가지 포인트
- 심플한 문법:
assert문만으로 테스트를 작성할 수 있는 학습 비용이 낮은 프레임워크 - fixture로 테스트를 스마트하게 관리:전처리·후처리·데이터 공유를 선언적으로 기술할 수 있다
- parametrize로 망라성을 높인다:하나의 함수로 복수의 패턴 테스트를 효율적으로 실행
테스트 자동화 학습 로드맵에서 pytest는 Python과 함께 가장 먼저 습득해야 할 프레임워크입니다. Playwright·Selenium 등의 자동화 도구와 조합하여 사용하는 경우가 대부분이며, 테스트 실행·관리·리포트 출력까지 한 손에 담당합니다. 이 글에서는 설치부터 실무에서 사용할 수 있는 수준까지 단계별로 상세히 해설합니다.
pytest란?unittest와의 차이
pytest는 Python용 오픈소스 테스트 프레임워크입니다. 표준 라이브러리의 unittest에 비해 코드 작성량이 적고, 에러 메시지가 읽기 쉬우며, 풍부한 플러그인 생태계를 갖추고 있습니다.
| 비교 항목 | unittest | pytest |
|---|---|---|
| 작성 스타일 | 클래스 상속 필요 | 함수 기반(클래스 불필요) |
| 어서션 | assertEqual, assertTrue 등 | assert문 하나로 완결 |
| 에러 메시지 | 단순함 | 상세하고 읽기 쉬움 |
| 픽스처 | setUp / tearDown | @pytest.fixture(유연함) |
| 플러그인 | 적음 | pytest-html, allure 등 풍부함 |
| 외부 도구 호환성 | 보통 | Playwright·Selenium과 최상 |
① 설치·환경 구축
먼저 pytest를 설치합니다. Python 3.8 이상이 필요합니다.
# pytest 설치
pip install pytest
# 버전 확인
pytest --version
# 자주 사용하는 플러그인을 한 번에 설치
pip install pytest pytest-html pytest-xdist프로젝트 구성 예시:
my_project/
├── src/
│ └── calculator.py # 테스트 대상 코드
├── tests/
│ ├── conftest.py # 공통 픽스처 보관소
│ ├── test_calculator.py # 테스트 파일
│ └── test_api.py
└── pytest.ini # pytest 설정 파일test_로 시작하거나(또는 _test.py로 끝나는)명명 규칙을 따라야 합니다. pytest는 이 규칙에 따라 테스트를 자동 검색합니다.② 기본 테스트 작성법
먼저 테스트 대상이 될 간단한 함수를 준비합니다.
# src/calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def divide(a, b):
if b == 0:
raise ValueError("0으로 나눌 수 없습니다")
return a / b다음으로 대응하는 테스트를 작성합니다:
# tests/test_calculator.py
import pytest
from src.calculator import add, subtract, divide
# ✅ 기본 테스트 함수(함수명은 test_로 시작)
def test_add_정상():
result = add(3, 5)
assert result == 8
def test_subtract_정상():
result = subtract(10, 4)
assert result == 6
def test_divide_정상():
result = divide(10, 2)
assert result == 5.0
# ✅ 예외가 발생하는 것을 테스트하기
def test_divide_제로_나눗셈():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "0으로 나눌 수 없습니다" in str(exc_info.value)테스트 실행
# 모든 테스트 실행
pytest
# 특정 파일만 실행
pytest tests/test_calculator.py
# 상세 표시(-v)
pytest -v
# 테스트명으로 필터링(-k)
pytest -k "divide"
# 첫 번째 실패에서 중지(-x)
pytest -x실행 결과 샘플
========================= test session starts ==========================
platform win32 -- Python 3.11.0, pytest-7.4.0
collected 4 items
tests/test_calculator.py::test_add_정상 PASSED [ 25%]
tests/test_calculator.py::test_subtract_정상 PASSED [ 50%]
tests/test_calculator.py::test_divide_정상 PASSED [ 75%]
tests/test_calculator.py::test_divide_제로_나눗셈 PASSED [100%]
========================== 4 passed in 0.12s ===========================③ fixture(픽스처)사용법
fixture는 테스트의 전처리·후처리·공유 데이터를 관리하는 pytest의 핵심 기능입니다. 매번 동일한 설정 코드를 작성할 필요가 없어지며, 테스트 코드가 깔끔해집니다.
# tests/test_user.py
import pytest
# --- fixture 정의 ---
@pytest.fixture
def sample_user():
"""테스트용 사용자 데이터를 반환하는 fixture"""
return {
"id": 1,
"name": "테스트 사용자",
"email": "test@example.com",
"role": "admin"
}
@pytest.fixture
def empty_list():
return []
# --- fixture를 사용한 테스트 ---
def test_user_name(sample_user):
assert sample_user["name"] == "테스트 사용자"
def test_user_role(sample_user):
assert sample_user["role"] == "admin"
def test_empty_list_is_empty(empty_list):
assert len(empty_list) == 0setup / teardown(전처리·후처리)
yield를 사용하면 테스트 전후 처리를 하나의 fixture에 통합할 수 있습니다:
@pytest.fixture
def db_connection():
# --- 전처리(테스트 시작 전) ---
print("\n[SETUP] DB에 연결 중...")
connection = {"status": "connected", "db": "test_db"}
yield connection # ← 여기서 테스트에 전달
# --- 후처리(테스트 종료 후) ---
print("\n[TEARDOWN] DB 연결을 닫았습니다")
connection["status"] = "disconnected"
def test_db_is_connected(db_connection):
assert db_connection["status"] == "connected"fixture의 스코프(scope)
| scope | 실행 타이밍 | 용도 |
|---|---|---|
function(기본값) | 테스트 함수마다 | 가벼운 데이터 준비 |
class | 클래스마다 | 클래스 단위 공유 |
module | 파일마다 | 파일 내에서 재사용 |
session | 테스트 전체 실행에서 1회 | DB 연결·브라우저 기동 등 무거운 처리 |
# 세션 전체에서 한 번만 브라우저를 기동하는 예(Playwright 연동)
@pytest.fixture(scope="session")
def browser():
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
yield browser
browser.close()④ parametrize(파라미터화 테스트)
@pytest.mark.parametrize를 사용하면 동일한 테스트 로직을 복수의 패턴으로 실행할 수 있습니다. 경계값 테스트나 정상계·비정상계 망라에 매우 유효합니다.
import pytest
from src.calculator import add, divide
# ✅ 기본 파라미터화
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # 양의 정수
(-1, -2, -3), # 음의 정수
(0, 5, 5), # 0 포함
(100, 200, 300) # 큰 수
])
def test_add_파라미터화(a, b, expected):
assert add(a, b) == expected
# ✅ 복수의 비정상계를 한 번에 테스트
@pytest.mark.parametrize("a, b", [
(10, 0),
(0, 0),
(-5, 0),
])
def test_divide_제로_나눗셈_파라미터화(a, b):
with pytest.raises(ValueError):
divide(a, b)실행하면 파라미터마다 독립된 테스트로 표시됩니다:
tests/test_calculator.py::test_add_파라미터화[1-2-3] PASSED
tests/test_calculator.py::test_add_파라미터화[-1--2--3] PASSED
tests/test_calculator.py::test_add_파라미터화[0-5-5] PASSED
tests/test_calculator.py::test_add_파라미터화[100-200-300] PASSED⑤ conftest.py 활용
conftest.py는 복수의 테스트 파일에서 fixture를 공유하기 위한 특별한 파일입니다. 여기에 작성한 fixture는 import 없이 모든 테스트에서 참조할 수 있습니다.
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def base_url():
"""모든 테스트에서 공유하는 베이스 URL"""
# 실제 코드에서는 http:// 를 붙여주세요(예:"http://localhost:8080")
return "localhost:8080"
@pytest.fixture
def test_user_credentials():
"""테스트용 로그인 정보"""
return {
"username": "standard_user",
"password": "secret_sauce"
}
@pytest.fixture(scope="session")
def api_headers():
"""API 테스트용 공통 헤더"""
return {
"Content-Type": "application/json",
"Accept": "application/json"
}# tests/test_login.py
# ← conftest.py의 fixture는 import 없이 그대로 사용 가능!
def test_login_with_valid_credentials(test_user_credentials, base_url):
username = test_user_credentials["username"]
password = test_user_credentials["password"]
# ... 테스트 로직⑥ 마크(mark)활용
pytest의 마크 기능을 사용하면 테스트 스킵이나 조건부 실행을 간단하게 할 수 있습니다.
import pytest
import sys
# ✅ 테스트 스킵
@pytest.mark.skip(reason="미구현으로 인해 임시 스킵")
def test_미구현_기능():
assert some_future_function() == True
# ✅ 조건부 스킵(Windows에서만 스킵하는 등 OS 의존 테스트에 유효)
@pytest.mark.skipif(sys.platform == "win32", reason="Windows 미지원")
def test_linux_only():
assert True
# ✅ 기존 버그(실패가 예상되는 테스트)
@pytest.mark.xfail(reason="버그 수정 대기 중: issue #123")
def test_known_bug():
assert 1 + 1 == 3 # 의도적으로 실패
# ✅ 커스텀 마크(그룹화하여 선택 실행 가능)
@pytest.mark.smoke
def test_로그인_스모크테스트():
assert True
@pytest.mark.regression
def test_결제_플로우_회귀테스트():
assert True# 스모크 테스트만 실행
pytest -m smoke
# 회귀 테스트만 실행
pytest -m regression
# 커스텀 마크는 pytest.ini에 등록 필요
# pytest.ini:
# [pytest]
# markers =
# smoke: 스모크 테스트
# regression: 회귀 테스트⑦ pytest.ini 설정
프로젝트 루트에 pytest.ini를 두면 pytest 전체 동작을 커스터마이즈할 수 있습니다:
# pytest.ini
[pytest]
# 테스트 파일 검색 경로
testpaths = tests
# 기본 옵션(매번 -v --tb=short --html=... 를 붙이는 것과 동일)
addopts = -v --tb=short --html=reports/report.html --self-contained-html
# 커스텀 마크 등록(경고를 없애기 위해 필요)
markers =
smoke: 스모크 테스트(최소한의 동작 확인)
regression: 회귀 테스트
slow: 실행 시간이 긴 테스트
# 로그 출력 설정
log_cli = true
log_cli_level = INFO⑧ HTML 리포트 출력
pytest-html 플러그인을 사용하면 테스트 결과를 HTML 파일로 출력할 수 있습니다. CI/CD에서의 결과 공유나 QA 리포트로서 매우 편리합니다.
# 설치
pip install pytest-html
# HTML 리포트 출력
pytest --html=reports/report.html --self-contained-html
# pytest.ini에 추가하면 매번 자동 생성
# addopts = -v --html=reports/report.html --self-contained-html생성된 리포트는 reports/report.html을 브라우저에서 열면 확인할 수 있습니다. 테스트명·합격 여부·실행 시간·에러 상세가 일람 표시됩니다.
--self-contained-html 옵션을 붙이면 CSS와 JS가 하나의 파일에 포함되어 이메일이나 Slack으로 공유하기 쉬워집니다.⑨ Playwright × pytest 조합 예시
pytest는 Playwright와 조합함으로써 E2E 테스트를 보다 정리된 형태로 관리할 수 있습니다. pytest-playwright 플러그인을 사용하는 것이 가장 간단합니다.
# 설치
pip install pytest-playwright
playwright install chromium# tests/test_saucedemo.py
import pytest
from playwright.sync_api import Page
# pytest-playwright가 제공하는 page fixture를 인자로 받기만 하면 OK
# 실제 코드에서는 page.goto()의 인자에 http:// 를 붙여주세요(예:http://localhost:8080/)
def test_로그인_정상(page: Page):
page.goto("localhost:8080/")
page.fill("#user-name", "standard_user")
page.fill("#password", "secret_sauce")
page.click("#login-button")
assert page.url.endswith("/inventory.html")
def test_로그인_비밀번호_오류(page: Page):
page.goto("localhost:8080/")
page.fill("#user-name", "standard_user")
page.fill("#password", "wrong_password")
page.click("#login-button")
error_msg = page.locator(".error-message-container").text_content()
assert "Epic sadface" in error_msg# 헤드리스 모드로 실행(기본값)
pytest tests/test_saucedemo.py -v
# 브라우저를 표시하여 실행(디버그 시 편리)
pytest tests/test_saucedemo.py --headed
# 브라우저 지정
pytest tests/test_saucedemo.py --browser firefox⑩ 자주 사용하는 커맨드 정리
| 커맨드 | 설명 |
|---|---|
pytest | 모든 테스트 실행 |
pytest -v | 상세 표시 모드 |
pytest -x | 첫 번째 실패에서 즉시 중지 |
pytest -k "키워드" | 테스트명으로 필터 실행 |
pytest -m smoke | 마크로 필터 실행 |
pytest -s | print문 출력 표시 |
pytest --lf | 이전에 실패한 테스트만 재실행 |
pytest -n 4 | 4병렬로 실행(pytest-xdist) |
pytest --html=report.html | HTML 리포트 출력(pytest-html) |
FAQ
Q. pytest와 unittest 중 어느 것을 사용해야 하나요?
새 프로젝트나 학습 목적이라면 pytest를 강력히 추천합니다. 코드 작성량이 적고, 에러 메시지가 읽기 쉬우며, Playwright나 Selenium과의 호환성도 최상입니다. 기존 unittest 코드는 pytest로도 그대로 실행할 수 있으므로 마이그레이션도 단계적으로 진행할 수 있습니다.
Q. conftest.py는 어디에 두면 되나요?
프로젝트 규모에 따라 다르지만, 기본적으로 tests/conftest.py에 두면 됩니다. 서브디렉토리에도 둘 수 있으며, 해당 디렉토리 내 테스트에만 적용됩니다. 대규모 프로젝트에서는 계층적으로 배치하여 스코프를 나누는 것이 베스트 프랙티스입니다.
Q. 테스트가 느린 경우 어떻게 하면 되나요?
pytest-xdist를 사용하여 병렬 실행하는 것이 가장 효과적입니다(pytest -n auto). 또한 무거운 초기화 처리는 scope="session"의 fixture에 정리하여 테스트 전체 실행에서 한 번만 실행되도록 하면 대폭 단축됩니다.
Q. pytest는 CI/CD에서도 사용할 수 있나요?
네, GitHub Actions·GitLab CI·Jenkins 등 주요 CI/CD 도구와의 호환성이 최상입니다. 커맨드 라인으로 실행할 수 있으므로 YAML의 run: pytest 한 줄로 통합할 수 있습니다. HTML 리포트를 아티팩트로 저장하는 것도 간단히 할 수 있습니다.
⚠️ pytest에서 자주 발생하는 함정 7선
실무에서 pytest를 사용하기 시작할 때 많은 사람이 걸려드는 포인트를 정리했습니다. 미리 파악해 두면 불필요한 디버깅 시간을 대폭 줄일 수 있습니다.
① 테스트 파일·함수명이 test_로 시작하지 않는다
pytest는 파일명이 test_*.py이고 함수명이 test_로 시작하는 것만 자동 검색합니다. check_login()이나 validate_form()처럼 작성해도 테스트로 인식되지 않아 실행해도 collected 0 items라고 표시됩니다.
② conftest.py의 위치를 잘못 지정해 fixture가 읽히지 않는다
conftest.py는 pytest가 자동으로 읽지만, 테스트 파일과 같은 디렉토리나 그 상위 계층에 두어야 합니다. 실무에서 가장 많은 원인은 tests/ 폴더 밖에 두는 케이스입니다.
project/
├── src/
└── tests/
├── conftest.py ← 여기에 둔다(tests/ 바로 아래)
└── test_api.py③ scope="session"의 fixture에서 상태가 인계되어 테스트가 서로 영향을 준다
세션 스코프의 fixture는 테스트 전체 실행에서 한 번만 초기화됩니다. 그 때문에 어떤 테스트가 fixture의 상태를 덮어쓰면 후속 테스트에 영향이 미칩니다. 특히 리스트나 딕셔너리 등의 뮤터블 데이터를 fixture에서 반환하는 경우 주의가 필요합니다. API 테스트에서 응답 데이터를 fixture에 저장하고 덮어쓰면 다른 테스트가 실패하는 원인이 됩니다.
④ 커스텀 마크를 pytest.ini에 등록하지 않으면 경고가 나온다
@pytest.mark.smoke와 같은 커스텀 마크를 사용하더라도 pytest.ini의 [pytest] markers =에 등록하지 않으면 PytestUnknownMarkWarning 경고가 계속 출력됩니다. CI 환경에서는 경고가 에러로 처리되는 경우도 있으므로 반드시 등록하세요.
⑤ print()의 출력이 터미널에 표시되지 않는다
pytest는 기본적으로 표준 출력을 캡처하므로 테스트 내의 print()가 보이지 않습니다. 디버깅 시 확인하려면 pytest -s 옵션을 붙여 실행하세요. pytest.ini의 addopts에 -s를 추가하면 항상 표시할 수 있지만 CI 환경에서는 로그가 너무 많아지는 점에 주의하세요. 또한 테스트가 실패한 경우에는 print()의 출력이 자동으로 표시됩니다.
⑥ assert 전에 예외가 발생해 테스트 결과가 「ERROR」가 된다
테스트가 「FAILED」가 아닌 「ERROR」가 되어 있을 때는 assert에 도달하기 전에 예기치 않은 예외가 발생한 것입니다. fixture의 초기화 실패·import 에러·타입 에러가 원인인 경우가 많습니다. pytest -v --tb=long으로 스택 트레이스를 자세히 확인하세요.
⑦ 프로젝트 루트 이외에서 실행하여 ModuleNotFoundError가 발생한다
초보자가 반드시 한 번은 빠지는 에러가 이것입니다. tests/ 폴더 안으로 이동한 후 pytest를 실행하면 src 모듈을 찾지 못해 ModuleNotFoundError: No module named 'src'가 발생합니다.
# ❌ NG:tests 폴더 안에서 실행
cd tests
pytest
# ✅ 권장:프로젝트 루트에서 실행
cd my_project
pytestpytest는 항상 프로젝트 루트(pytest.ini가 있는 디렉토리)에서 실행하는 것이 기본입니다.
📖 관련 글
📋 이 글의 정리
- pytest는 심플한
assert문만으로 테스트를 작성할 수 있는 Python 최강의 테스트 프레임워크 - fixture로 전처리·후처리·공유 데이터를 선언적으로 관리하여 코드 중복을 제거할 수 있다
- parametrize로 경계값·정상계·비정상계를 효율적으로 망라하여 테스트 품질을 높일 수 있다
- conftest.py에 공통 fixture를 정리하면 복수의 테스트 파일에서 재사용할 수 있어 유지보수성이 높아진다
- mark로 테스트를 그룹화하여 스모크·회귀·스킵을 유연하게 컨트롤할 수 있다
- pytest-html로 리포트를 자동 생성하여 CI/CD나 팀과의 공유에 활용할 수 있다
pytest를 마스터하면 Playwright나 Selenium과의 조합으로 본격적인 테스트 자동화 기반을 구축할 수 있습니다. 먼저 심플한 함수 테스트부터 시작하여 fixture·parametrize·conftest.py를 단계적으로 익혀나가세요.

