올리브영 테크블로그 포스팅 UI 테스트 자동화 구조
QA Engineer

UI 테스트 자동화 구조

유지 보수를 쉽게 해볼까요?

2023.11.11

안녕하세요.

올리브영 품질관리를 담당하는 QA파트의 Hyun입니다.

올리브영 QA파트는 수동 테스트 이외에 UI 테스트 자동화, HealthCheck, 성능 테스트를 수행하고 있습니다.

그중 이번 글에서는 UI 테스트 자동화 구조에 대해 공유하고자 합니다.


아래에 올리브영 로그인을 수행하는 UI 테스트 자동화 스크립트가 있습니다.

스크립트가 짧고 간결하기 때문에 많은 분들이 UI 테스트 자동화 스크립트를 아래와 같이 작성합니다.

하지만, 만약 테스트 스크립트가 수십 혹은 수백 개일 경우에는 어떨까요?

보기에도 힘들고 동일한 요소를 여러 군데에서 사용하는 중에 해당 요소가 변경된다면 해당되는 스크립트 모두 변경해야되기 때문에

시간이 오래 걸리는 것 뿐만 아니라, 예기치 못한 오류가 발생하기 쉽습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
 
 
chrome_options = webdriver.ChromeOptions()
desired_caps = {}
desired_caps['automationName'= 'UiAutomator2'
desired_caps['platformName'= 'Android'
desired_caps['platformVersion'= '12.0'
desired_caps['deviceName'= 'XXX'
desired_caps['udid'= 'XXX'
desired_caps['browserName'= 'Chrome'
 
desired_caps.update(chrome_options.to_capabilities())
 
driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
 
try:
    # 1. 올리브영 메인 홈 화면에 진입한다.
    driver.get('https://www.oliveyoung.co.kr')
    # 2. 하단 탭바 '마이페이지' 버튼을 탭한다.
    driver.find_element(By.XPATH,"//a[@class='Tabbar_user__yQjMj']").click()
    # 3. 유효한 아이디를 입력한다.
    driver.find_element(By.XPATH, "//input[@id='loginId']").send_keys("ID")
    # 4. 유효한 비밀번호를 입력한다.
    driver.find_element(By.XPATH, "//input[@id='password']").send_keys('PASSWORD')
    # 5. 로그인 버튼을 탭한다.
    driver.find_element(By.XPATH, "//button[@class='btnGreen']").click()
    
    # 드라이버 종료
    driver.quit()
 
except Exception as e:
    print('  Failed to compile, exception is %s' % repr(e))
cs

이러한 문제를 해결하기 위한 방법은 요소를 별도의 클래스 파일에 만드는 것입니다.

그렇게 된다면 필요시 해당 요소를 모든 스크립트에서 재사용할 수 있고, 요소가 변경이 된다면 해당 요소만 찾아서 변경만 하면 됩니다.

이러한 접근 방식을 페이지 개체 모델(POM) 이라고 합니다.


페이지 개체 모델(POM) :

페이지에 대한 인터페이스 역할을 하는 개체 지향 클래스로 페이지의 UI와 상호 작용해야 할 때마다 이 페이지 개체 클래스의 메서드를 사용합니다.

UI가 변경되면 테스트 자체는 변경할 필요가 없고 내부 코드만 변경할 수 있다는 이점이 있으며, 페이지 개체를 변경하면 새 UI를 지원하기 위한 모든 변경 사항이 한곳에 위치합니다.


페이지 개체 모델 구조는 다음과 같습니다. pom
페이지 개체 모델(POM)



이해하기 쉽도록 구조도 및 간단한 예시를 보여드리겠습니다.

├── TestCases
│   ├──__init__.py
│   │── conftest.py
│   └── test_mypage.py
├── page_objects
│   ├── __init__.py
│   └── Mypage.py
└── Utilities
│   └── __init__.py
└── config.py


페이지 객체 (Page Objects):

각 웹 페이지는 페이지 객체로 표현되며, 해당 페이지의 요소와 동작이 포함됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import time
 
from selenium.common import NoSuchElementException, UnexpectedAlertPresentException
from selenium.webdriver.common.by import By
from PageObjects.basePage import BasePage
 
 
class MyPage(BasePage):
 
    def __init__(self, driver):
        super().__init__(driver)
 
    # 하단 '마이페이지' 아이콘
    login_icon_new = "//a[@class='Tabbar_user__yQjMj']"
    # ID 텍스트 박스
    id_textbox = "//input[@id='loginId']"
    # Password 텍스트 박스
    password_textbox = "//input[@id='password']"
    # 로그인 버튼
    login_button = "//button[@class='btnGreen']"
 
    def tap_mypage_button(self):
        self.tap(By.XPATH, self.login_icon_new)
        
    def input_id(self, text):
        self.clear(self.id_textbox)
        self.input(By.XPATH, self.id_textbox, text)
 
    def input_password(self, text):
        self.input(By.XPATH, self.password_textbox, text)
 
    def tap_login_button(self):
        self.tap(By.XPATH, self.login_button)
        self.wait_element(By.XPATH, self.login_name)
               
cs

위 예시 중 "ID 텍스트 박스"의 요소가 "//input[@id='loginId']" 에서 "//input[@id='loginID']" 로 변경이 있을 경우,

"//input[@id='loginID']" 요소만 변경해 주면 해당 요소를 사용하는 객체들도 반영이 되기 때문에 유지 보수가 원활합니다.


테스트 코드:

테스트 코드는 페이지 객체를 사용하여 웹 페이지와 상호 작용하고 테스트를 수행합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from PageObjects.loginPage import LoginPage
from PageObjects.myPage import MyPage
from Utilities.readProperties import ReadConfig
from config import loginData
 
 
class Test_MyPage:
    baseURL = ReadConfig.getProductURL()
    mypageTitleLabel = ReadConfig.getMypageText()
 
    def testverify_mypage_load(self, setup):
 
        try:
            self.driver = setup
            login = LoginPage(self.driver)
            mypage = MyPage(self.driver)
 
            # 1. 올리브영 메인 홈 화면에 진입한다.
            url = self.baseURL
            self.driver.get(url)
            # 2. 하단 탭바 '마이페이지' 버튼을 탭한다.
            login.tap_login_icon_new()
            # 3. 유효한 아이디를 입력한다.
            login.input_id(loginData.id)
            # 4. 유효한 비밀번호를 입력한다.
            login.input_password(loginData.password)
            # 5. 로그인 버튼을 탭한다.
            login.tap_login_button_otherpage()
            # 마이페이지 진입 여부 확인
            if mypage.get_mypage_title == self.mypageTitleLabel:
                assert True
            else:
                assert False
        except Exception as e:
            print('  Failed to compile, exception is %s' % repr(e))
 
cs

※ 참고 :

loginData 즉, 로그인 데이터가 보관되어 있는 장소는 개인정보 영역이기 때문에 별도로 config.py에서 호출하고 gitignore를 통해 git에 업로드되지 않도록 했습니다.

어떤 코드 테스트를 사용하는지에 따라 구조가 변경될 수 있겠지만, 저희는 pytest를 통해 코드 테스트를 수행하고 있습니다.

pytest는 TDD를 하기 위한 프레임워크로 conftest.py에서 fixture를 통해 웹 드라이버를 실행을 선언하고, 테스트 수행 및 웹 드라이버를 사용할 때마다 해당 fixture를 이용할 수 있게 했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import pytest, os, configparser
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from Utilities.readProperties import ReadConfig
 
 
@pytest.fixture()
def setup(env):
    platformName= config.get(env, 'platformName')
    platformVersion = config.get(env, 'platformVersion')
    deviceName = config.get(env, 'deviceName')
    automationName = config.get(env, 'automationName')
    browserName = config.get(env, 'browserName')
    udid = config.get(env,'udid')
    remote = config.get(env,'remote')
    driver = None
 
    chrome_options = webdriver.ChromeOptions()
 
    capabilities = {
        'automationName': automationName,
        'platformName': platformName,
        'platformVersion': platformVersion,
        'deviceName': deviceName,
        'browserName': browserName,
        'udid': udid,
        'prod': prod
    }
    capabilities.update(chrome_options.to_capabilities())
 
def pytest_addoption(parser): # CLI/hooks 를 통해 device 값 받음
    parser.addoption("--env", action="store", default="Web", help="device")
 
 
@pytest.fixture()
def env(request): # setup method에 browser 값 return
    return request.config.getoption("--env")
            
 
 
cs

실행방법 :

pytest test_mypage.py --env=mobile

마치며

지금까지 올리브영 QA 파트에서 UI 테스트 자동화 구조 예시를 공유드렸습니다.

글 읽어주셔서 고맙습니다. 🙇‍♂

올리브영 QA Enginner로 일하고 싶다면 지원하러 가기



이미지 출처 : https://dev.to/annequinkenstein/selenium-design-a-framework-page-object-pattern-4g55

QA
올리브영 테크 블로그 작성 UI 테스트 자동화 구조
🎋
Hyun |
QA Engineer
협업해 보아요~!