Pytest, POM, DDT, 리포트 자동화: QA 자동화 테스트 흐름 정리

2025. 4. 14. 10:35 ·

📁 Automation

1. 테스트 프레임워크란?

자동화 테스트를 작성할 때 가장 처음 고민해야 하는 게 "어떤 도구로 테스트를 짤까?" 이고
이때 사용하는 게 바로 테스트 프레임워크예요.

쉽게 말하면, 테스트를 구조화하고, 실행하고, 결과까지 정리해주는 도구라고 보면 돼요.


1-1. 왜 테스트 프레임워크를 써야 할까?

일반 코드처럼 짤 수 있는데 왜 프레임워크를 써야 하지? 라고 생각이 들 수 있는데

예시와 이유를 설명해 드릴게요.

  • `test_login()` 이런 함수로도 테스트는 가능하지 않을까? → 실행 순서 제어가 안돼요.
  • 결과는 `console.log()`로 찍어보면 되잖아? → 수동 확인이 필요해요.
  • 실패했는지 성공했는지 내가 판단하면 되지! → 자동화의 의미가 없어져요.

1-2. 테스트 구조 이해하기

프레임워크를 쓰면 아래처럼 테스트를 더 "체계적"으로 할 수 있어요.
describe('로그인 기능 테스트', () => {
  beforeEach(() => {
    // 매 테스트마다 로그인 페이지로 이동
    goToLoginPage();
  });

  it('아이디와 비밀번호 입력 후 로그인 버튼 클릭 시 메인 페이지로 이동해야 한다', () => {
    typeId('admin');
    typePassword('1234');
    clickLogin();

    expect(getCurrentUrl()).toBe('/main');
  });

  afterEach(() => {
    // 테스트 끝난 후 쿠키 삭제
    clearCookies();
  });
});
  • `describe`: 전체 테스트 그룹 (기능 단위로 묶기 좋아요)
  • `it`: 개별 테스트 케이스
  • `expect()`: 검증할 조건 (Assertion)
  • `beforeEach` / `afterEach`: 준비 & 정리 작업

👇 Hook 함수란?

더보기
"테스트 실행 전후에 자동으로 실행되는 함수" 를 말해요.
일종의 테스트 준비/정리 도우미 같은 역할이에요.
중복 코드를 줄이고, 테스트 흐름을 예측 가능하게 만들어줘요.

언제 쓰면 좋을까?

  • 반복되는 초기화 작업이 있을 때
  • 공통적으로 마무리해야 할 정리 작업이 있을 때
  • 테스트 실행 흐름을 관리하고 싶을 때

대표적인 hook 함수 4가지 (JS 기준)

이 중에서도 가장 자주 쓰이는게 바로 이거예요:

  • `beforeEach`: 각 테스트 시작 전에 매번 실행
  • `afterEach`:  각 테스트 끝난 후에 매번 실행

beforeEach / afterEach 예시:

describe("로그인 테스트", () => {
  beforeEach(() => {
    console.log("🔁 테스트 시작 전 실행됨");
  });

  afterEach(() => {
    console.log("✅ 테스트 끝난 후 실행됨");
  });

  it("로그인 성공 케이스", () => {
    console.log("🚀 로그인 테스트 실행");
  });

  it("로그인 실패 케이스", () => {
    console.log("❌ 에러 메시지 확인");
  });
});

실행 순서 출력: 

🔁 테스트 시작 전 실행됨
🚀 로그인 테스트 실행
✅ 테스트 끝난 후 실행됨

🔁 테스트 시작 전 실행됨
❌ 에러 메시지 확인
✅ 테스트 끝난 후 실행됨

"훅(Hook)"이랑 "픽스처(Fixture)"는 같은 건가요?

  • 겹치는 부분이 있지만 서로 다른 개념이에요.
  • `pytest`에서는 fixture가 hook처럼 활용될 수 있어서 헷갈리기 좋은 구조에요.

  • Hook = 실행 타이밍 제어
  • Fixture = 실행에 필요한 값을 준비
  • `pytest`에서는 Fixture가 Hook처럼 작동할 수도 있음
    → `scope='function'`을 주면 테스트마다 실행되고 Hook처럼 사용 할 수 있어요.

Python (pytest) 예시:

import pytest

# ⬇️ 이건 "픽스처": 테스트에 필요한 드라이버를 만들어 넘겨줌
@pytest.fixture
def createDriver():
    driver = ChromeDriver()
    yield driver
    driver.quit()

# ⬇️ 이건 "훅"처럼 쓰임: 픽스처가 자동 실행됨 (테스트마다 실행됨)
def test_login(createDriver):
    driver = createDriver
    driver.get("https://example.com")

`@pytest.fixture`는 실제로 훅처럼 동작하지만,
진짜 목적은 “공통 자원 전달”이에요.

# Hook 예시 (setup/teardown처럼 동작)
def setup_function():
    print("테스트 시작 전에 실행")

def teardown_function():
    print("테스트 끝난 후 실행")

# Fixture 예시
@pytest.fixture
def sample_data():
    return {"user": "daye"}

def test_sample(sample_data):
    assert sample_data["user"] == "daye"

코드를 실제로 실행하면 테스트 전후 메시지와 테스트 결과가 출력돼요.

아래는 pytest로 실행했을 때 나오는 결과 예시예요:

테스트 시작 전에 실행
.
테스트 끝난 후 실행

======================= test session starts =======================
collected 1 item

test_example.py .                                         [100%]

======================== 1 passed in 0.01s ========================

코드에 쓴 `setup_function()`과 `teardown_function()`이
pytest의 hook처럼 테스트 전/후에 자동으로 호출된 걸 확인할 수 있어요.


beforeAll / beforeEach / afterEach / afterAll 예시:

describe("로그인 테스트", () => {
  // 테스트 전체 시작 전에 딱 1번 실행
  beforeAll(() => {
    launchBrowser(); // 브라우저 한 번만 켜기
  });

  // 각 테스트 케이스 전에 매번 실행
  beforeEach(() => {
    goToLoginPage(); // 매 테스트마다 로그인 페이지로 이동
  });

  it("아이디/비번 입력 후 로그인 성공", () => {
    typeId("user");
    typePassword("1234");
    clickLogin();
    expect(getUrl()).toBe("/main");
  });

  it("비밀번호 없이 로그인 시 에러 메시지 출력", () => {
    typeId("user");
    clickLogin();
    expect(getError()).toBe("비밀번호를 입력해주세요.");
  });

  // 각 테스트 케이스 끝날 때마다 실행
  afterEach(() => {
    clearCookies(); // 매 테스트 후 쿠키 삭제
  });

  // 모든 테스트 끝난 뒤 딱 1번 실행
  afterAll(() => {
    closeBrowser(); // 테스트 끝나면 브라우저 닫기
  });
});

2. 페이지 객체 모델 (POM)

테스트 자동화에서 페이지마다 클래스 하나씩 만들어서 요소와 동작을 분리해서 관리하는 방식이에요.

자동화 테스트가 길어지고 복잡해질수록
"이 버튼 어디 있었지?", "클릭은 어디서 정의하지?"
헷갈리는 일이 자주 생겨요. 

POM(Page Object Model)은 테스트 코드를 더 읽기 쉽고, 수정도 편하게 만들어줘요.


2-1. 폴더 구조 

project_root/
├── src/
│   ├── pages/
│   │   └── home.py               # 페이지 기능 정의
│   ├── utils/
│   │   └── homeLocators.py       # 요소들 위치 정의
├── tests/
└── └── test_home.py              # 테스트 시나리오 작성
  • 페이지 클래스 (`home.py`): 그 페이지에 있는 기능을 모아둔 클래스
  • Locators 파일 (`homeLocators.py`): 페이지 안 요소들의 위치 정보만 따로 저장
  • 테스트 파일 (`test_home.py`): 실제 테스트 시나리오 수행하는 곳

2-2. 예시 코드 흐름

2-2-1. Locators 

# src/utils/homeLocators.py
from selenium.webdriver.common.by import By

LOGIN_BTN = (By.ID, "loginBtn")
EMAIL_INPUT = (By.NAME, "email")
PASSWORD_INPUT = (By.NAME, "password")

2-2-2. Page 객체 

# src/pages/home.py
class HomePage:
    def __init__(self, driver):
        self.driver = driver

    def login(self, email, pw):
        self.driver.find_element(*EMAIL_INPUT).send_keys(email)
        self.driver.find_element(*PASSWORD_INPUT).send_keys(pw)
        self.driver.find_element(*LOGIN_BTN).click()
 

__init__() 생성자와 self는 왜 필요할까?

Selenium + Pytest로 테스트 코드를 짜다 보면,`__init__`, `self`, `driver`, `createDriver` 같은 단어들이 등장해요.처음엔 이게 서로 어떻게 연결되는지, 왜 `self`를 써야 하는지,어떤 값이 어디서 자동으로 넘

dayedev.tistory.com

2-2-3. 테스트 파일

# tests/test_home.py
from src.pages.home import HomePage

def test_login(createDriver):
    home = HomePage(createDriver)
    home.login("test@example.com", "1234")

정리하자면

  • 요소는 Locators 파일에만 (수정할 때 여기만 보면 돼요.)
  • 동작은 Page 파일에만 (클릭, 입력 같은 행동 정의해요.)
  • 시나리오는 테스트 파일에만 (사람 눈에 읽히게 작성해요.)

3. 데이터 기반 테스트 (DDT)

똑같은 테스트 로직에 여러 데이터를 넣어서 반복 테스트할 수 있게 해주는 방식이에요.

예시:

@pytest.mark.parametrize("input, expected", [
    ("daye", "안녕하세요, daye님!"),
    ("minho", "안녕하세요, minho님!"),
])
def test_greet(input, expected):
    assert greet(input) == expected
  • `@pytest.mark.parametrize` 데코레이터를 써서
  • `test_greet`라는 하나의 함수가
  • 다양한 입력값(`input`)과 기대값(`expected`)을 받아서
  • 자동으로 여러 번 실행돼요.

👇 로그인 테스트 예시

더보기
@pytest.mark.parametrize("id, pw", [
    ("user1", "pw1"),
    ("user2", "pw2"),
])
def test_login(createDriver, id, pw):
    driver = createDriver
    driver.get("https://testsite.com/login")

    driver.find_element(By.ID, "email").send_keys(id)
    driver.find_element(By.ID, "password").send_keys(pw)
    driver.find_element(By.ID, "submit").click()

    assert "로그인 성공" in driver.page_source

이렇게 하면 동일한 로그인 로직에
다양한 계정 데이터를 넣어 테스트할 수 있어요.


👇 POM vs DDT 비교

  • POM = 기능(동작)을 묶어서 관리 & 재사용성 확보
  • DDT = 데이터(입력값)를 바꿔가며 반복 테스트 & 커버리지 향상
더보기

POM 예시: pages/loginPage.py

class LoginPage:
    def __init__(self, driver):
        self.driver = driver

    def go_to_login(self):
        self.driver.get("https://testsite.com/login")

    def login(self, username, password):
        self.driver.find_element(By.ID, "email").send_keys(username)
        self.driver.find_element(By.ID, "password").send_keys(password)
        self.driver.find_element(By.ID, "submit").click()


DDT 예시: tests/test_login.py

@pytest.mark.parametrize("username, password", [
    ("user1", "pw1"),
    ("user2", "pw2"),
])
def test_login(createDriver, username, password):
    loginPage = LoginPage(createDriver)
    loginPage.go_to_login()
    loginPage.login(username, password)
    assert "로그인 성공" in createDriver.page_source
  • `LoginPage`는 POM이고,
  • `@pytest.mark.parametrize`는 DDT예요.

  • POM은 테스트 코드의 구조를 정리하고, 유지보수를 쉽게 해줘요.
  • DDT는 테스트를 다양한 데이터로 반복 실행하게 해줘요.
  • 둘은 상호 보완 관계이고, 실무에서는 둘 다 같이 써서
    “깔끔하게 관리되고, 다양한 상황을 커버할 수 있는 테스트”를 만들어요.

파라미터 쓰면 DDT 인가요?

`@pytest.mark.parametrize` 데코레이터를 사용해서
하나의 테스트 함수에 여러 데이터를 주입해서 반복 실행하게 만들면,
그게 바로 데이터 기반 테스트(DDT, Data-Driven Testing) 예요.

그러니까 parametrize = DDT라고 봐도 돼요.


`@pytest.mark.parametrize`가 뭐냐면?

테스트 함수에 다양한 데이터를 반복해서 넣어주는 장치예요.
즉, 함수는 딱 한 번만 작성하지만,
그 함수가 여러 개의 입력 조합으로 반복해서 실행돼요.


예시:

@pytest.mark.parametrize("x, y", [
    (1, 2),
    (3, 4),
    (5, 6)
])
def test_add(x, y):
    assert x + y < 10

함수 하나로 테스트가 총 3번 돌아가요.

  1. test_add(1, 2)
  2. test_add(3, 4)
  3. test_add(5, 6)

4. JavaScript 활용법 (도우미 함수, 모듈화, 직접 제어 등)

Selenium 단독으론 부족할 때, JS를 보조 수단으로 사용해요.


4-1. 도우미 함수로 JS 실행

어떤 요소가 화면엔 분명 있어 보이는데
클릭이 안 될 때, 스크롤이 안 될 때,

그럴 땐 JS로 직접 DOM을 조작해줘야 해요.

  • 예시 ①: 클릭이 안 되는 요소 직접 클릭
driver.execute_script("arguments[0].click();", element)
element.click()은 안 먹히지만 JS 클릭은 강제로 실행돼요.

  • 예시 ②: 뷰포트 밖에 있는 요소 스크롤로 가져오기
driver.execute_script("arguments[0].scrollIntoView(true);", element)
화면 아래쪽에 있는 버튼을 강제로 화면에 끌어올리는 거에요.

👇 arguments[0]이란?

더보기
JavaScript 안에서 외부(Python)로부터 전달받은 첫 번째 인자를 뜻해요.
driver.execute_script("arguments[0].click();", element)​
여기서 `element`는 Python에서 넘긴 값이고,
`arguments[0]` 자리에 들어가서 JS 코드 안에서 실제 DOM 요소처럼 쓰여요.

1개만 넘기면? → arguments[0]

driver.execute_script("arguments[0].click();", element)​
여러 개 넘기면? → arguments[1], arguments[2]도 가능해요.
scroll_target = driver.find_element(By.ID, "footer")
click_target = driver.find_element(By.CLASS_NAME, "go")

driver.execute_script("""
    arguments[1].scrollIntoView(true);
    arguments[0].click();
""", click_target, scroll_target)
즉, arguments[n]은 "JS 내부에서" 사용하는 거고, 값은 “파이썬에서 넘기는 순서”로 매칭돼요.
  • arguments[0] → click_target
  • arguments[1] → scroll_target

세 번째 요소를 클릭하고 싶다면?

Python에서 리스트로 요소들을 가져온 경우라면,
먼저 Python에서 세 번째 요소를 선택한 후 JS에 넘겨줘야 해요.

elements = driver.find_elements(By.CSS_SELECTOR, "div.list-item")
third_element = elements[2]  # 인덱스는 0부터 시작!
driver.execute_script("arguments[0].click();", third_element)​

4-2. 모듈화로 반복되는 JS 함수 분리하기

자주 쓰는 JS 로직은 Python 함수처럼 묶어두면 좋아요.

# utils/js_helper.py
def js_scroll_to(driver, element):
    driver.execute_script("arguments[0].scrollIntoView(true);", element)

def js_click(driver, element):
    driver.execute_script("arguments[0].click();", element)

그리고 테스트 코드에서 이렇게 호출해요.

from utils.js_helper import js_click, js_scroll_to

js_scroll_to(driver, target_element)
js_click(driver, target_element)
유지보수성도 좋아지고, 코드도 더 깔끔해져요.

4-3. 직접 JS 코드 작성해서 원하는 기능 실행하기

  • 예시 ①: 페이지의 타이틀을 가져오기
title = driver.execute_script("return document.title")
print(title)  # 콘솔에 현재 페이지 제목 출력
  • 예시 ②: 특정 속성값 읽기
value = driver.execute_script("return arguments[0].getAttribute('value');", input_element)

👇 부모-자식 구조 / 형제 요소 탐색 방법

더보기
1. 부모 안의 자식 요소 클릭
예시:
// 예: 부모 요소인 card 안에 있는 "상세보기" 버튼 클릭
let card = document.querySelector('.card')
card.querySelector('.detail-btn').click()​

Selenium에서 실행한다면:

driver.execute_script("arguments[0].querySelector('.detail-btn').click()", parent_element)​

2. 형제 요소 탐색

  • 다음 형제 요소 nextElementSibling
  • 이전 형제 요소 previousElementSibling
arguments[0].nextElementSibling.click()
arguments[0].previousElementSibling.click()

Selenium 사용 예:

driver.execute_script("arguments[0].nextElementSibling.click()", current_element)

3. 특정 자식 요소가 여러 개일 때, 인덱스로 선택

arguments[0].querySelectorAll('li')[2].click()

`li`가 여러 개 있을 때, 세 번째 항목 클릭해요.


4. 부모 → 자식 → 또 그 자식

arguments[0]
  .querySelector('.list')
  .querySelector('button')
  .click()

5. 테스트 리포팅 & 디버깅

문제의 원인을 파악하고 결과를 한눈에 보는 핵심 도구예요.


5-1. 테스트 리포팅 (Test Reporting)

  • 테스트 결과를 보기 쉽게 요약 정리
  • 실패한 테스트가 왜 실패했는지 로그로 확인
  • 팀원/리더에게 자동 보고서로 공유

5-1-1. pytest-html

설치:

pip install pytest-html

실행:

pytest --html=report.html

결과:

  • `report.html` 파일 생성 → 브라우저에서 열면 정리된 결과 확인 가능해요.

5-1-2.Allure Report (고급 시각화 리포트)

설치:

pip install allure-pytest
brew install allure  # mac 기준

실행:

pytest --alluredir=./allure-results
allure serve ./allure-results
  • 테스트 케이스별 결과가 나와요.
  • 에러 메시지 / 스크린샷 첨부 가능해요.
  • 브라우저에서 리포트 실시간 확인 가능해요.

5-2. 디버깅 (Debugging)

  • 테스트 실패 원인을 정확히 분석
  • 실제 UI가 어떻게 동작했는지 추적


5-2-1. 로깅(logging)

  • 테스트 코드에 로그 메시지를 남겨 흐름 추적해요.
import logging

logger = logging.getLogger("test_log")
logger.info("페이지 진입 성공")
logger.warning("요소가 없음")
logger.error("로그인 실패 발생")
  • `conftest.py`에서 공통 logger 설정하면 좋아요.
  • 각 테스트 파일에서 logger 가져다 쓰면 돼요.

5-2-2. 스크린샷 저장

  • 테스트 실패한 순간의 화면을 저장해서 시각적으로 확인해요.
driver.save_screenshot("error_로그인.png")
  • 실패한 테스트 케이스마다 자동 저장되게 설정 가능해요.
  • Allure / HTML 리포트에 스크린샷 첨부도 가능해요.

저작자표시 비영리 변경금지 (새창열림)

'📁 Automation' 카테고리의 다른 글

프론트와 백이 소통하는 방법: HTTP, API 개념 정리  (2) 2025.04.14
Selenium과 자바스크립트로 요소 찾는 방법  (0) 2025.04.14
__init__() 생성자와 self는 왜 필요할까?  (0) 2025.04.11
Selenium과 자바스크립트 비동기 처리 예제: 로그인 자동화  (1) 2025.04.09
Selenium과 자바스크립트에서 비동기 사용 이유: 에러 처리 방법(try/catch/finally)  (4) 2025.04.08
'📁 Automation' 카테고리의 다른 글
  • 프론트와 백이 소통하는 방법: HTTP, API 개념 정리
  • Selenium과 자바스크립트로 요소 찾는 방법
  • __init__() 생성자와 self는 왜 필요할까?
  • Selenium과 자바스크립트 비동기 처리 예제: 로그인 자동화
당메
당메
모든 게시글은 PC에 최적화 되어 작성되어 있습니다 :-)
  • 당메
    it종사자 기록
    당메
  • 글쓰기 피드 관리
    • 분류 전체보기 (28)
      • 📁 Project (1)
      • 📁 Automation (18)
      • 📁 Conference (2)
      • 📁 Custom (5)
      • 🎀 Hobby (2)
  • 링크

    • 이전 블로그 보러가기
    • 정보처리기사 합격
    • ISTQB 합격
  • hELLO· Designed By정상우.v4.10.3
당메
Pytest, POM, DDT, 리포트 자동화: QA 자동화 테스트 흐름 정리
글쓰기 상단으로

티스토리툴바