Selenium

정태경·2022년 4월 2일
2
post-thumbnail

오래간만에 혼자 있는 시간이 생겨 Selenium을 다뤄보았다.
이전에는 많이 사용했었지만 오랫동안 사용하지 않았었기 때문에 추후 다시 활용할 일이 생겼을 때 찾아볼 수 있도록 기록해두려 한다. 오늘의 목표는 내가 담당하고 있는 서비스의 로그인부터 결제 그리고 예약 취소 플로까지의 자동화 테스트 구현이다.

1. 웹 드라이버 셋팅

예전에는 크롬 버전에 맞는 웹 드라이버를 찾아서 직접 다운로드하는 번거로운 과정이 있었는데, 요즘엔 ChromeDriverManager가 이 과정을 다 알아서 해준다.

from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)

2. 패키지 구조

Pytest를 활용하면 보다 다양한 기능을 활용할 수 있겠지만 오늘은 간단하게 다뤄볼 예정이라 unittest를 활용해서 테스트 코드를 작성해 보았다. 기본적인 디렉터리 구조는 다음과 같다.

├── Config
│   ├── Accounts.py                     # 계정 정보
│   ├── Products.py                     # 자동화 테스트에 사용할 상품 정보
│   └── __init__.py
├── Pages
│   ├── Base.py                         # Page Object 공통으로 사용되는 메서드 구현
│   ├── PageEmailSignIn.py              # Page Object - 이메일 로그인 (Pages.Base.py 상속)
│   ├── PageLodgingDetail.py            # Page Object - 민박 상세페이지 (Pages.Base.py 상속)
│   ├── PageOrder.py                    # Page Object - 주문서 페이지 (Pages.Base.py 상속)
│   ├── PageOrderResults.py             # Page Object - 결제 완료 페이지 (Pages.Base.py 상속)
│   ├── PageReservationDetail.py        # Page Object - 예약 상세 페이지 (Pages.Base.py 상속)
│   ├── PageSignIn.py                   # Page Object - 회원가입 페이지 (Pages.Base.py 상속)
│   ├── __init__.py
└── TestCases
    ├── Base.py                         # setUpClass, tearDownClass 등 테스트 케이스 공통으로 사용되는 베이스 모듈
    ├── TestLodgingPurchase.py          # 민박 결제 테스트케이스 (TestCases.Base.py 상속)
    ├── __init__.py

3. 페이지 오브젝트에서 공통으로 사용되는 메서드 구현

기본적으로 Selenium 라이브러리에서 제공하는 메서드만 사용해서 개발하다 보면 몇 가지 불편한 점들이 있다. Exception 핸들링이나 Retry 로직 등을 위해 모든 페이지에서 공통적으로 사용하는 메서드들을 별도로 구현해두었다.

앞으로 생성될 모든 Page Object는 BasePage를 상속받도록 구현할 예정이다.
따라서 click, send_keys, find_element처럼 공통으로 사용되는 메서드는 모두 BasePage 내의 메서드를 사용하고, 각 페이지별 모듈에서는 그 페이지에서만 사용되는 엘리먼트, 메서드만 별도로 정의하면 된다.

# Pages.Base.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException, TimeoutException


class BasePage:

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

    def get(self, url):
        """ URL 이동 """
        self.driver.get(url)

    def click(self, locator):
        """ 클릭 """
        try:
            WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(locator)).click()
        except NoSuchElementException:
            raise NoSuchElementException("NoSuchElementException : %s" % str(locator))
        except TimeoutException:
            raise TimeoutException("TimeoutException : %s" % str(locator))

    def find_element(self, locator):
        """ 엘리먼트 찾기 """
        try:
            WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(locator))
            return self.driver.find_element(*locator) 
        except NoSuchElementException:
            raise NoSuchElementException("NoSuchElementException : %s" % str(locator))
        except TimeoutException:
            raise TimeoutException("TimeoutException : %s" % str(locator))
        # 로깅 추가 필요함

    def find_elements(self, locator):
        """ 엘리먼트 찾기 (배열로 리턴) """
        try:
            WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(locator))
            return self.driver.find_elements(*locator)
        except NoSuchElementException:
            raise NoSuchElementException("NoSuchElementException : %s" % str(locator))
        except TimeoutException:
            raise TimeoutException("TimeoutException : %s" % str(locator))

    def send_keys(self, locator, value):
        """ 인풋 필드 값 입력 """
        self.find_element(locator).send_keys(value)

4. 각 페이지별 모듈 생성

아래 코드는 이메일 로그인 페이지의 엘리먼트와 이메일 로그인 페이지에서 할 수 있는 액션을 간단한 메서드로 구현해두었다. 3번 항목에서 설명하였듯 BasePage를 상속받고 있다.

# Pages.PageEmailSignin.py
from Pages.Base import BasePage
from selenium.webdriver.common.by import By
from Config.Accounts import Accounts


class EmailSignIn(BasePage):

    input_email_id = (By.NAME, "user[email]")
    input_email_pw = (By.NAME, "user[password]")
    btn_login = (By.XPATH, '//button[text()="이메일로 로그인"]')

    sign_in_url = "https://www.myrealtrip.com/users/email_sign_in"

    def __init__(self, driver):
        super(EmailSignIn, self).__init__(driver)

    def get_login_page(self):
        self.get(self.sign_in_url)

    def send_keys_email_id(self):
        self.send_keys(self.input_email_id, Accounts["email_id"])

    def send_keys_email_pw(self):
        self.send_keys(self.input_email_pw, Accounts["email_pw"])

5. 테스트 케이스 작성을 위해 unittest 셋팅

간단하게 테스트하기 위해 unittest를 사용하여 구현하였다.
setUpClass는 테스트 클래스가 시작되기 이전 단 한 번 호출되는 메서드이다. 따라서 크롬 드라이버를 세팅할 수 있도록 코드를 작성하였다.
tearDownClass는 테스트 클래스가 종료되고 나면 단 한 번 호출되는 메서드이다. 테스트 중간에 브라우저를 닫기 싫어서 모든 테스트가 종료되고 나서만 닫히도록 코드를 작성하였다.

Selenium의 공식 문서를 보면 테스트마다 브라우저를 재실행해야 한다고 권장하고 있는듯하니 추후에 참고하면 좋을 듯.

# TestCases.Base.py
import unittest

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager


class BaseTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls) -> None:
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--headless")
        cls.driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)

    def setUp(self) -> None:
        pass

    def tearDown(self) -> None:
        pass

    @classmethod
    def tearDownClass(cls) -> None:
        cls.driver.quit()

6. 테스트 케이스 작성

유저 사용 시나리오에 따라 테스트 케이스를 작성하였고, 테스트 케이스마다 assertion으로 해당 테스트 케이스가 성공인지 실패인지 검증하도록 하였다.

import time

from TestCases.Base import BaseTest
from Pages.PageEmailSignIn import EmailSignIn
from Pages.PageLodgingDetail import LodgingDetail
from Pages.PageOrder import Order
from Pages.PageOrderResults import OrderResults
from Pages.PageReservationDetail import ReservationDetail


class LodgingPurchase(BaseTest):

    def test_1_email_signin(self):
        email_signin = EmailSignIn(self.driver)
        email_signin.get(email_signin.sign_in_url)
        email_signin.send_keys_email_id()
        email_signin.send_keys_email_pw()
        email_signin.click(email_signin.btn_login)
        self.assertEqual(self.driver.current_url, "https://www.myrealtrip.com")

    def test_2_lodging_detail_access(self):
        lodging_datail = LodgingDetail(self.driver)
        lodging_datail.get(lodging_datail.product_url)
        self.assertEqual(lodging_datail.get_product_title(), "제주도 한옥 민박")

    def test_3_select_options(self):
        lodging_detail = LodgingDetail(self.driver)
        lodging_detail.select_calendar_date()
        lodging_detail.select_room()
        self.assertEqual("예약하기", lodging_detail.find_element(LodgingDetail.btn_reservation).text)
        lodging_detail.click(lodging_detail.btn_reservation)

    def test_4_order(self):
        order = Order(self.driver)
        order.click(order.btn_point_use_all)
        order.send_keys_kor_name()
        order.send_keys_eng_name()
        order.send_keys_extra_info()
        order.click(order.btn_purchase)
        self.assertIn("success", self.driver.current_url)

    def test_5_order_results(self):
        order_results = OrderResults(self.driver)
        order_results.click(order_results.btn_reservation_detail)
        self.assertIn("reservations", self.driver.current_url)

    def test_6_order_cancel(self):
        reservation_detail = ReservationDetail(self.driver)
        reservation_detail.cancel_reservation()
        self.assertEqual("예약취소", reservation_detail.get_reservation_status())

결과 확인

시간이 없어서 HTML Report나 Allure Report까지는 구현하지 못했다. 아쉽지만 IDE 자체에서 테스트 실행 후 Assertion 통과되는 것까지 확인한 것으로 만족해야 할 듯하다. 추후에 이런 시간이 또 나에게 주어진다면 테스트 리포트 생성과 테스트 병렬 실행 등까지 구현해 보아야겠다.

profile
現 두나무 업비트 QA 엔지니어, 前 마이리얼트립 TQA 엔지니어

4개의 댓글

comment-user-thumbnail
2022년 11월 3일

좋은 내용 잘 봤습니다.

답글 달기
comment-user-thumbnail
2022년 12월 13일

지나가다 발견하여 좋은 글 읽게 되었습니다. 아직 셀레니움 초보자인데 Pages.Base.py 파일에서 driver.get 이나 find_element 명령어가 활성화가 되지 않는데 이유가 무엇인지 알 수 있을까요?

1개의 답글