QA Automation/pytest-playwright

pytest-playwright 기본 프로젝트 / skeleton project / 기본구조 설명

kokorii_ 2024. 11. 25. 02:02

pytest-playwright 프로젝트 새로 세팅하면서 정리해보는 내가 쓰는 나만의 프로젝트 구조 ...


0. 프로젝트 설명 

POM을 적용, pytest-playwright framework를 사용하여 테스트 코드를 작성하고, allure report를 생성합니다.

1. 프로젝트 구조

- 사용하는 패키지는 requirements에 작성되기때문에 생략

project
 ┣ logs
 ┣ pages
 ┃ ┣ __init__.py
 ┃ ┗ main_page.py
 ┣ tests
 ┃ ┣ __init__.py
 ┃ ┗ test_main.py
 ┣ users
 ┃ ┣ __init__.py
 ┃ ┗ uesr.py
 ┣ utils
 ┣ conftest.py
 ┣ pytest.ini
 ┣ readme.md
 ┗ requirements.txt

2. pytest.ini

log, cmd-option, 환경변수 등 pytest 실행과 관련된 설정을 작성한다

2-1) 로그

따로 로거를 만들어서 등록할 필요 없이 여기서 Log 옵션을 작성하면된다 

물론 필요에따라 만들어서 붙여서도 된다. 

나는 두개 다 사용하다가 ... 뭣하러 그러나 싶어서 시원하게 print 쓴다 .. ㅋㅋㅋㅋㅋㅋㅋㅋ

2-2) 재실행 옵션

네트워크 지연 등 특수한 이유로 한번씩 테스트가 튕길 수 있기 때문에 나는 최소 2번까지는 실행해본다. 

자동 rerun으로 1을 주면 총 2회 실행되고, 테스트 재실행 시 1초의 시간간격을 갖는다

2-3) 실행옵션

테스트 실행할때마다 모든 옵션을 다 쓰는게 번거롭기도 하고, 그렇게 쓰다보면 

 

"내가 지금 slowmo를 준건가? 몇으로줬었지?"

"지금 -m이었나 -k 었나?"

"아 나 지금 -vv 안줬네 ㅋ" 등 ....

 

촘촘따리 거슬리는 부분들이 생겨서 addopts에 공통요소는 미리 설정하고 시작한다 

나 혼자 할때는 그나마 나만 혼란스러운거니까 그나마 괜찮은데, 협업자가 있을때는 꼭 옵션은 통일해서 사용하는게 좋다

2-4) pytest markers

pytest에서 내가 가장~ 좋아하는 기능은 요 마커기능 

테스트 만들어서 마커붙여두면 마커끼리만 실행할 수 있다. 

https://docs.pytest.org/en/stable/example/markers.html

 

기능별로 expired_email, 용도별로 by_regression 등등 다양하게 사용할 수 있다

2-5) 테스트 변수 사용

ini에 테스트 데이터를 작성하고 conftest에서 불러서 fixture로 사용할 수 있다..!! 

 

이전에 환경변수를 config.json으로 작성해서 타겟별로 변경하며 사용하는 방법을 포스팅한적이 있는데, 그땐 .... 이걸 몰랐어 ...

근데 두개 나름대로의 장점과 단점이 있어서 둘다 사용해보고 있는 중이다 

json 방식은 익숙하다는 장점이 가장 크다. 편집이 쉽고 프로젝트를 다른 프레임워크로 바꿔도 json load 형식으로 사용하면되서 간편하다(어떤 환경에서든 파일만 갖고다니면됨!!)

 

ini방식은 파일을 더이상 만들지 않아도된다는 점 외에......... 음 ....

json 보다 작성 방식이 더 간단하다...?

 

결론: 나는 둘다 만족스럽다

[pytest]
log_cli = true
log_cli_level = DEBUG
log_cli_format =[%(process)d %(asctime)s %(filename)s %(funcName)s %(lineno)d] %(message)s
log_cli_date_format=%Y-%m-%d %H:%M:%S
reruns = 1
reruns_delay = 1
addopts = -s -vv --alluredir=allure-results

markers = 
	use_regression: using when a quick result is needed, for regression or checking report.

[url]
base_url = base_url.com/
login = login

[account]
id = kokori
pwd = password

3. conftest.py

pytest 내부 실행과 관련되어 미리 정의된 fixture나 hook을 지정할 수 있는 파일이다

내가 기본으로 사용하는 Fixture는 5~6개이고, 나는 테스트 페이지를 재사용하는 구조를 좋아해서 테스트 페이지를 모두 fixture로 설정하여 사용한다

3-1) 테스트 변수 사용을 위한 configure

ini파일에서 작성한 테스트 변수를 읽어오기 위해 pytest_configure(config)와 config_data()를 사용한다

pytest_configure의 경우 슬랙 메세지 발신을 위해 reporter클래스를 등록할 때도 쓴다. 

3-2) 테스트의 시작과 끝

pytest_sessionstart(session), pytest_sessionfinish(session) 이 두가지는 기본으로 쓰고 시작하는데

아무 동작도 하지 않더라도 "테스트 시작", "테스트 끝" 을 명시하는 것만으로도 큰 정보를 얻을 수 있다 

테스트가 많지 않을때는 의미가 없는데, 테스트가 많아지면 어디가 세션의 시작이고 끝인지가 불분명해지고, 특히 병렬 실행하면 이걸로 코드 추적이 용이해질때가 많아서 나는 꼭꼭 사용한다 

테스트 세션 끝날때 allure 리포트 생성해주면 편하다

3-3)페이지별 fixture 사용

나는 페이지별로 fixture를 사용하는 것을 좋아한다 

어떤 페이지는 테스트에 로그인이 필요하고, 어떤 페이지는 필요하지 않고, 또 어떤 페이지는 특정 계정으로 로그인해야만 테스트가 가능하다. 

코드 재사용성, 가독성, 그리고 내가 가장 중요하게 생각하는 페에지 객체는 격리되어 관리해야한다는 점을 가장 잘 표현한 구조라 선호한다 

 

프로젝트 구조 만들면서 처음 리서치할때 unlogin_page, login_page와 같이 사용하는 경우도 보긴했다 

import configparser
import os

import pytest
from datetime import datetime
from playwright.sync_api import Page

from pages.main_page import MainPage

today = datetime.today()
log_directory = './logs'
os.makedirs(log_directory, exist_ok=True)


@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
    config_data.ini = configparser.ConfigParser()
    config_data.ini.read("pytest.ini")


@pytest.fixture(scope="session", autouse=True)
def config_data():
    """INI 파일에서 설정값을 읽어와 반환하는 fixture"""
    ini_file = configparser.ConfigParser()
    ini_file.read("pytest.ini")
    return ini_file


def pytest_sessionstart(session):
    if os.path.exists('logs/count_of_result.txt'):
        with open('logs/count_of_result.txt', 'w') as f:
            f.write('')

    print("테스트를 시작합니다")


def pytest_sessionfinish(session, exitstatus):
    print()

    os.system("allure generate --single-file allure-results -o allure-report --clean")

    print("테스트가 끝났습니다")



@pytest.fixture(scope="function")
def main_page(page: Page, config_data):
    test_page = MainPage(page, config_data)

    test_page.page.goto(config_data.get("url", "base_url"))

    yield test_page
    page.close()

 

---- [안중요함] 페이지별 fixture 구조에 대한 나의 개인적인 고민과 고민과 고민 

아직 fixture가 너무 많아져서 불편할 정도는 아니라 단점은 잘 못느끼고 있지만, 늘 속도면에서 이게 최선일까? 라는 고민이 있다

한 세션안에서 페이지객체를 session 단위로 사용하는게 아니라 function 단위로 사용하고 있는데 이것도 늘 고민이다 

예를 들어 "마이페이지 > 회원정보 수정" 페이지를 테스트한다고 했을 때 테스트를 잘게 쪼개서 assertion을 주고싶으면 fixture를 session으로 사용하는게 맞을까? 

 

그럼 "회원정보 수정"을 위한 mark나 옵션을 달고, 테스트 프로세스를 분리시켜줘야할까? 

그럼 run_script.py가 별도로 생성되고, 멀티 프로세스를 위한 실행부 구성을 다시해주고, 프로세스별로 분리되어 나오는 테스트 결과나 로그를 합쳐서, 아 근데 테스트 화면 녹화도 해야하고, allure도 봐야하니까 .................... blahblahblah .......... 

 

이런 연쇄고민지옥에 빠져있는 프로젝트가 하나 있는 요즘이다 ㅎ........ 어뜨카지..............

4. Page class file

가장 핵심인 Page, Test 파일 중 Page 파일이다 

Page 클래스에는 말 그대로 테스트하려는 페이지를 코드로 받아쓰기해둔 형태라고 볼 수 있다 

버튼 id, xapth, locator, get_by_role 등 ... 모든 방법을 총 동원해서 페이지의 구조를 클래스에 담아낸다

 

클래스안에 상호작용이 필요한 메소드를 만드는 경우도 있지만, 나는 거의 만들지 않는다

페이지와 상호작용하는 동작은 이미 playwright에 잘 정의 되어있다. 클릭, 더브클릭, 뒤로가기, 앞으로가기, 마우스 이동, 스크롤 등등 ...

 

이건 작성하는 사람이 정하기 나름이긴하지만, 페이지 클래스는 최대한 단순한게 좋다고 생각해서 구성요소들만 표현하려고한다

 

config_data를 클래스 초기화할때 함께사용하는건, 테스트 변수에 대한 의존성을 최대한 줄이기 위해서였~다

목적은 그랬다.

필요한 account 정보나 url만을 페이지 객체 초기화 시 세팅하는게 깔끔하지않을까~? 라는 ^_^ 엄청난 이론이 있었어

 

(지금 실무 프로젝트 config를 여기저기 마구 박아뒀다. 과거의 내가 ㅎ.. 왜그랬을까 ...) 

from playwright.sync_api import Page


class MainPage:
    def __init__(self, page, config_data: dict):
        self.page: Page = page
        self.config_data: dict = config_data

    def btn_login(self):
        return self.page.locator("#signin_button")

5. Test File

테스트 파일이다. 페이지 객체의 구성요소들이 원하는대로 잘 동작하는지 시나리오를 작성한다. 

 

아래 예시코드는 버튼을 눌렀을 때 내가 원하는 url로 잘 이동하는지 검사하는 시나리오다. 

re.compile로 러프하게 검사하고 있는데, 빡빡하게(?) 보수적으로 검사하고 싶다면 

re.compile을 제거하고 base_url + login 으로 검사할 수 있다(완벽하게 동일한 url로 이동하는지)

import re

from playwright.sync_api import expect

from pages.main_page import MainPage


def test_main_01(main_page: MainPage):
    main_page.btn_login().click()

    expect(main_page.page).to_have_url(re.compile(main_page.config_data.get("url", "login")))

user, utils는 매번 사용하진 않아서 생략했다 

user의 경우 페이지를 객체단위로 테스트하고 있으니, user를 한 10명 만들어서 테스트할 수 있지않을까?

그럼 페르소나를 좀 더 구체적으로 테스트에 접목시켜 사용할 수 있지 않을까? 테스트가 좀 더 라이브(?)해지면 좋겠어!

 

라는 나의 또 야심찬 계획이 있긴한데 Hㅏ 

 

요새 조금 어리를빗 힘이가 들어서 지금까지 했던것들이라도 제대로 정리하려고 노력중이다.

 

하고싶은건 왜이렇게 많은걸까?  이욕심은 언제쯤끝나는거지?

언제쯤 지금 하는거나 똑바로하게될까 (....)