오프라인 폐쇄망에서 웹 테스트 자동화, 그런데 셀레니움을 곁들인

dropKick·2025년 2월 19일

개발 이슈

목록 보기
14/14

개요

  • 쿼리 튜닝을 통해 페이지 로딩 속도를 개선했으나, 실제 체감 속도도 그런지 확인이 필요해졌음
  • 적은 트래픽으로는 의미가 없을 것 같아 동시 접속 테스트를 진행하기 위해 웹 테스트 자동화 도구로 셀레니움을 선정
  • 오프라인 폐쇄망에서 구성 시 발생하는 약간의 이슈들을 기록

환경설정

  • Chrome 96
  • Python 3.13
  • VSCode 1.84.2
  • Chrome Driver 96

폐쇄망 환경에서 VSCode Python 확장 구성하기

확장 목록

✅ Python
✅ Python Debuger
✅ Pylance

폐쇄망에서 VSCode 확장 구성

  • 마켓플레이스에서 VSIX 파일 형태로 다운로드 후 VSIX 확장 설치
    • 가장 쉬운 방법인데 아쉽게도 위 3개 라이브러리는 VSIX로 제공을 해주지 않고있음
  • 온라인망에서 확장 구성 후 설정과 파일 가져오기
    • 이번에 선택한 방법으로 온라인망과 오프라인망의 의존성 환경은 동일해야함

폐쇄망으로 특정 확장만 옮기기

1. 온라인망 확장 설치

  • VSCode에서 확장 자동 설치
  • VSIX 파일을 통해 특정 릴리즈 설치

나는 최신 확장 버전이 아닌 특정 릴리즈가 필요했기 때문에 VSIX 파일을 찾았지만 제공이 되지 않아 VSCode 버전에 맞는 확장을 자동 설치했음

2. application.json 요소 추출

  • 확장 자동 설치의 경우 자동으로 해제된 VSIX 패키지를 구성하고, 의존성을 구성
  • 우리는 이미 설치된 확장을 옮기기 위해 확장의 의존성을 추출

VSCode 확장 경로

  • C:\Users\로컬-PC\.vscode\extensions
extension.json

{"version":"2024.3.2","location":{"$mid":1,"path":"/c:/Users/로컬-PC/.vscode/extensions/ms-python.vscode-pylance-2024.3.2","scheme":"file"},"relativeLocation":"ms-python.vscode-pylance-2024.3.2","metadata":{"id":"","publisherId":"","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1713315311220,"preRelease":false}}

이런 JSON 형태로 원하는 확장들이 정의 되어있는데
이 중 필요한 3개의 Python 확장 정보를 추출

3. 확장 옮기기

  • 확장 정보 추출까지 했다면 해당 확장 정보를 옮길 환경의 extension.json에 붙여넣기 후 extension 폴더에 풀려져있는 확장을 그대로 옮겨오면 자동으로 로드가 된다

폐쇄망 환경에서 pip 패키지 설치

설치 목록

  • selenium
  • webdriver_manager
    • 온라인 환경에서만 사용 가능
    • 크롬 드라이버 호환성을 잡아주는 아주 좋은 패키지..이나 폐쇄망에서는 못씀

whl을 이용한 pip 패키지 설치

온라인망 환경이라면 pip instal selenium 하나로 다 해결이 되었겠지만
폐쇄망에서는 타자를 치지 않는 자 패키지를 얻지 못하나니..
폐쇄망에서 패키지를 설치 가능한 whl 파일을 이용한 설치를 진행해야한다

패키지 의존성을 포함한 whl 파일 다운로드

pip download -d ./대상폴더

  • 패키지를 오프라인망으로 옮기기 위해 대상 폴더에 의존성을 포함하여 다운로드
  • d 대상 폴더 지정

다운로드 시 .whl 형태로 의존성을 포함한 파일들이 다운로드 되는데, 해당 파일을 압축하여 폐쇄망으로 옮기면 파일 이동은 끝난다

whl 파일로 pip install 진행

pip install --no-index -f *.whl ./

  • 설치는 간단하게 대상 폴더에 --no-index 옵션을 포함하여 pip install을 하면 종료
  • --no-index whl 순서가 아닌 의존성대로 설치
  • -f 대상 폴더 지정

폐쇄망 환경에서 셀레니움 환경 구성

이렇게 폐쇄망에서 파이썬 환경 구성 및 셀레니움 설치 시 실제로 셀레니움을 통한 테스트가 가능해진다

셀레니움 테스트 코드 구성 기준

  • 화면 구성이 DB 직접 접근이 아닌 API 조회를 통해 이루어지기에 화면 구성까지의 데이터의 송수신
  • 화면 자체의 렌더링 속도
  • 수신 데이터를 통한 화면 구성 속도
  • 브라우저 동시 호출에 따른 속도

웹 테스트 자동화 구성

원활한 테스트를 위한 셀레니움 옵션 설정

  • 테스트 및 headless 모드 사용 시 불필요한 옵션을 비활성화
  • 해당 옵션을 비활성화 하더라도 로그에는 남기 때문에 로깅 레벨 조정

보안 옵션 비활성화

    options.add_argument("--ignore-certificate-errors") # 인증서 무시
    options.add_argument("--ignore-ssl-errors") # SSL 무시
    options.add_argument("--ignore-certificate-errors-spki-list") # 인증서 공개키 에러 무시
    options.add_argument("--allow-insecure-localhost") # 로컬 SSL 오류 허용
    options.add_argument("--disable-web-security") # 웹 보안 비활성화
    options.add_argument("--disable-blink-features=AutomationControlled") # 봇 감지 비활성화
    options.add_argument("--disable-features=SSLHandshake") # SSL 핸드쉐이크 비활성화

GPU 옵션 비활성화

    options.add_argument("--disable-gpu") # GPU 가속 비활성화
    options.add_argument("--disable-software-rasterizer") # 소프트웨어 렌더링 비활성화
    options.add_argument("--disable-features=VizDisplayCompositor") # DirectComposition 비활성화
    options.add_argument("--disable-gpu-driver-bug-workarounds") # GPU 드라이버 에러 비활성화
    options.add_argument("--disable-skia-runtime") # Skia 렌더링 비활성화

하드웨어 옵션 비활성화

    options.add_argument("--disable-usb") # USB 검색 비활성화
    options.add_argument("--disable-webusb") # 웹 USB 비활성화
    options.add_argument("--disable-bluetooth") # 블루투스 비활성화
    options.add_argument("--disable-default-apps")
    options.add_argument("--no-default-browser-check") # 기본 브라우저 점검 비활성화

가상화 옵션 비활성화

    options.add_argument("--disable-dev-shm-usage") # 공유 메모리 비활성화
    options.add_argument("--no-sandbox") # 샌드박스 격리 비활성화
    options.add_argument("--disable-extensions") # 확장 비활성화

기타 옵션 및 로그 레벨 조정

    options.add_argument("--disable-popup-blocking") # 팝업 차단 비활성화
    # options.add_argument("--user-agent=CustomUserAgent") # 사용자 지정 에이전트 설정
    options.add_argument("--lang=ko") # 언어 설정
    options.add_argument("--log-level=3") # info 미만 로깅 비활성화

셀레니움 코드

  • 정해진 스레드 풀을 통해 테스트 페이지 호출
  • 테스트 페이지에서 테스팅 옵션 설정 후 실제 페이지 전환 호출
  • 페이지 호출 시 JS 렌더링 시간과 수신 데이터를 이용한 페이지 구성요소 렌더링 시간 측정
  • 이용 약관 동의/카드 선택을 거쳐 카드사 인증창 전환 호출
  • 카드사 인증창 내 선택 요소 호출

상기와 같은 형태로 단순히 카드를 선택, 호출하기까지의 속도 테스트 코드를 작성해보았음
결제 테스트 자동화까지의 영역은 브라우저 컨트롤 영역을 벗어나기 때문에 별도 글로 분리 작성 예정

import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Pool

TEST_URL = "https://test.co.kr/test/test.jsp"
USER_POOL = 100  # 동시 실행 스레드

def get_chrome_options(headless=True):
    options = webdriver.ChromeOptions()

    # 웹 보안 및 인증서 관련 설정
    options.add_argument("--ignore-certificate-errors") # 인증서 무시
    options.add_argument("--ignore-ssl-errors") # SSL 무시
    options.add_argument("--ignore-certificate-errors-spki-list") # 인증서 공개키 에러 무시
    options.add_argument("--allow-insecure-localhost") # 로컬 SSL 오류 허용
    options.add_argument("--disable-web-security") # 웹 보안 비활성화
    options.add_argument("--disable-blink-features=AutomationControlled") # 봇 감지 비활성화
    options.add_argument("--disable-features=SSLHandshake") # SSL 핸드쉐이크 비활성화

    # GPU 및 렌더링 관련 설정
    options.add_argument("--disable-gpu") # GPU 가속 비활성화
    options.add_argument("--disable-software-rasterizer") # 소프트웨어 렌더링 비활성화
    options.add_argument("--disable-features=VizDisplayCompositor") # DirectComposition 비활성화
    options.add_argument("--disable-gpu-driver-bug-workarounds") # GPU 드라이버 에러 비활성화
    options.add_argument("--disable-skia-runtime") # Skia 렌더링 비활성화

    # 하드웨어 및 장치 감지 비활성화
    options.add_argument("--disable-usb") # USB 검색 비활성화
    options.add_argument("--disable-webusb") # 웹 USB 비활성화
    options.add_argument("--disable-bluetooth") # 블루투스 비활성화
    options.add_argument("--disable-default-apps")
    options.add_argument("--no-default-browser-check") # 기본 브라우저 점검 비활성화

    # 가상화 환경 최적화
    options.add_argument("--disable-dev-shm-usage") # 공유 메모리 비활성화
    options.add_argument("--no-sandbox") # 샌드박스 격리 비활성화
    options.add_argument("--disable-extensions") # 확장 비활성화
    
    # 기타 옵션
    options.add_argument("--disable-popup-blocking") # 팝업 차단 비활성화
    # options.add_argument("--user-agent=CustomUserAgent") # 사용자 에이전트 설정
    options.add_argument("--lang=ko") # 언어 설정
    
    # 로그 레벨 조정
    options.add_argument("--log-level=3")

    # Headless 모드 설정
    if headless:
        options.add_argument("--headless")
        options.add_argument("--disable-features=TranslateUI")

    return options

def open_browser(uid):
    print(f"uid[{uid}] call browser")
    
    try:
        # Chrome 드라이버 호출
        options = get_chrome_options(headless=True)
        # options = get_chrome_options(headless=False)
        driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=options)
        driver.get(TEST_URL)
        print(f"uid[{uid}] open_browser: {driver.title}, 핸들: {driver.current_window_handle}")
        time.sleep(3) 

        # 테스트 페이지 iframe 전환
        iframe_el = driver.find_element(By.TAG_NAME, "iframe")
        driver.switch_to.frame(iframe_el)
        print(f"uid[{uid}] switch_to.frame: {driver.title}, 핸들: {driver.current_window_handle}") 

        # 결제 옵션 설정
        select_el = Select(driver.find_element(By.XPATH, "/html/body/form/table[4]/tbody[5]/tr[12]/td[2]/select"))
        select_el.select_by_value("Y") 
        
        amt_input_el = driver.find_element(By.XPATH, "/html/body/form/table[3]/tbody/tr[6]/td[2]/input")
        amt_input_el.clear()
        amt_input_el.send_keys("50000")

        # 결제하기 클릭
        driver.find_element(By.XPATH, "/html/body/form/p[1]/table/tbody/tr/td/input").click()

        # 팝업 결제창으로 윈도우 전환
        main_window = driver.current_window_handle # 현재 창 저장
        WebDriverWait(driver, 5).until(lambda d: len(d.window_handles) > 1) # 새 창 열림 대기
        page_popup_window = driver.window_handles[-1] # 가장 마지막 창
        driver.switch_to.window(page_popup_window)
        print(f"uid[{uid}] page_popup_window: {driver.title}, 핸들: {driver.current_window_handle}")
        
        # JS 렌더링 측정
        load_time = driver.execute_script(
             "return window.performance.timing.loadEventEnd - window.performance.timing.navigationStart"
        )
        print(f"uid[{uid}] JS 렌더링 소요 시간: {load_time / 1000:.3f} 초")

        # 렌더링 속도 측정
        start_time = time.time()
        WebDriverWait(driver, 5).until(
            EC.visibility_of_element_located((By.XPATH, "/html/body/div[2]/div[2]/div[2]/div[1]"))
        )
        end_time = time.time()
        find_el_time = end_time - start_time
        print(f"uid[{uid}] 페이지 렌더링 소요 시간: {find_el_time:.3f} 초")

        # 약관 동의 및 다음 버튼 클릭
        driver.find_element(By.XPATH, "//*[@id='card_agreement_all']").click()
        driver.find_element(By.XPATH, "//*[@id='card_payment_step1']/ul/li[9]/span[2]/button").click()

        # 카드 선택
        pick_card(driver, "KB", uid)

        # 다음 버튼 클릭
        driver.find_element(By.XPATH, "//*[@id='card_payment_step3']/ul/li[13]/span[2]/button").click()

        # ACS 팝업 전환
        WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > 1) # 새 창 열림 대기
        acs_popup_window = driver.window_handles[-1] # ACS 창
        driver.switch_to.window(acs_popup_window)
        print(f"uid[{uid}] acs_popup_window: {driver.title}, 핸들: {driver.current_window_handle}")
        
        # iframe 전환
        acs_frame_el = driver.find_element(By.TAG_NAME, "iframe")
        driver.switch_to.frame(acs_frame_el)
        print(f"uid[{uid}] acs_frame_el: {driver.title}, 핸들: {driver.current_window_handle}")

        # ACS 창 내 ISP 버튼 클릭 
        WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.XPATH, "//*[@id='tabView01']/div/ul/li[4]/button"))
        )
        driver.find_element(By.XPATH, "//*[@id='tabView01']/div/ul/li[4]/button").click()
        # focused_el = driver.execute_script("return document.activeElement;")
        # print(f"isp_btn_el: {isp_btn_el} focused_el: {focused_el}")
        print(f"uid[{uid}] isp_btn_el: {driver.title}, 핸들: {driver.current_window_handle}")

        # # 아이프레임 이전 복귀
        # driver.switch_to.default_content()
        # print(f"switch_to.default_content: {driver.title}, 핸들: {driver.current_window_handle}")

        # # 팝업 닫기
        # driver.close()
    except Exception as e:
        print(f"uid[{uid}] Exception {e}")
        
    finally:
        driver.switch_to.window(main_window)
        print(f"switch_to.default_content: {driver.title}, 핸들: {driver.current_window_handle}")
        driver.switch_to.default_content()
        driver.quit()
        print(f"uid[{uid}] exit browser")

def pick_card(driver, card_id, uid):
    driver.find_element(By.CSS_SELECTOR, f"input[type='radio'][value='{card_id}']").click()
    print(f"uid[{uid}] 카드 {card_id} 선택")

if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=USER_POOL) as executor:
        executor.map(open_browser, range(1, USER_POOL + 1))
profile
안아줘요

0개의 댓글