본문 바로가기
자동화

AI 기반 자가 치유형 Selenium 자동화 테스트

by 도천수 2025. 3. 13.

안녕하세요? 김명관입니다.
2024년 하반기에 저는 같은 팀 동료 류지원님과 함께 Selenium을 사용해 UI 자동화 테스트를 꾸려나가고 있었습니다. 자동화 테스트는 언제나 ROI를 따지게 되는데요, 최근에 그 성과를 한번 측정해 보기도 했었습니다.
원티드랩에서는 이번에 처음으로 팀 규모에서 기반부터 다져가며 UI 자동화 테스트를 만들어 나간지라 조사, 선정, 학습부터 시간이 어마어마하게 많이 들었습니다. 그 탓인지 시간을 기준으로 비교했을 때 ROI가 그리 좋게 나오지는 못했었습니다.
자동화 테스트에서 시간을 가장 많이 잡아먹는 작업은 유지보수라고 생각합니다. 많은 기업들도 유지보수의 벽을 최소화 하기위해 노력하고 있는데요. 이번에 성과를 측정하고 난 뒤 유지보수 시간을 더욱 줄이기 위해 여러가지 방법을 찾다 자가 치유 자동화 테스트 툴들에 대해 알게 되었습니다.


우리 코드에도 적용해볼 수 있겠는데?

자가 치유 자동화 테스트에 대한 개념을 알아가다 보니 OPENAI API를 이용한다면 우리의 코드에 직접 자가 치유 모듈을 달 수 있겠다는 생각이 들었어요. 대충 머리속으로 세워본 계획은 이랬습니다.

 1. Selenium 테스트 코드 실행 중 요소를 찾지 못하는 문제 발생.
 2. 자가 치유 모듈 개입해서 실패한 요소가 있는 페이지의 body를 전달.
 3. 실패한 요소의 기존 셀렉터와 이 셀렉터가 어떤 특징을 가지고 있는지 파라미터로 전달.
 4. GPT는 전달해준 body에서 나의 설명과 일치하는 요소 탐색.
 5. 해당 요소의 셀렉터를 만들어서 리턴.

제 계획이 먹혀들지 궁금해 간단하게 샘플코드를 만들어 보았더니 생각보다 새로운 셀렉터를 잘 만들어 주는 모습을 볼 수 있었어요.

# selenium 코드
import traceback
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from self_healing_e2e import self_healing
  

driver = webdriver.Chrome()
url = "{url}"

driver.get(url)

sign_in_button = driver.find_element(
	By.XPATH, "//button[contains(., '로그인')")
sign_in_button.click()

try:
    wait_email_login_button = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((
        	By.XPATH, "//h1[text()='안녕하세요?로그인 페이지 헤더입니다.']")))
        
except Exception as e:
    error_type = type(e).__name__
    print(f"에러 발생: {error_type} - {e}")

old_xpath = "//button[contains(., '로구읜')]" # 잘못된 셀렉터의 예시
element_description = "로그인 버튼" # 기존 셀렉터의 용도를 설명하는 디스크립션

try:
    driver.find_element(By.XPATH, old_xpath).click() # 잘못된 셀렉터로 클릭을 시도 -> 실패

except Exception as e:
    error_type = type(e).__name__
    print(f"에러 발생: {error_type} - {e}")

    soup = BeautifulSoup(driver.page_source, "html.parser") # 현재 페이지의 body를 받아옴
    for tag in soup(["script", "style", "meta", "link"]): # 기타 필요없는 부분을 제거해 토큰을 줄임
        tag.extract()
        
    body_text = soup.body.get_text(" ", strip=True)
    
    new_xpath = self_healing.healing(old_xpath, element_description, body_text) # 자가 치유 모듈에 파라미터로 전달

    try:
        driver.find_element(By.XPATH, new_xpath).click() # AI가 찾아준 셀렉터로 클릭을 재시도
        
    except Exception as e:
        error_type = type(e).__name__
        print(f"에러 발생: {error_type} - {e}")
        traceback.print_exc()
# 자가 치유 모듈
import openai
import re


def healing(xpath, element_description, page_source):
    print("start healing")
    
    API_KEY = {openai_api_key}

    # 최신 방식으로 OpenAI 클라이언트 생성
    client = openai.Client(api_key=API_KEY)
    prompt = "여기에 프롬프트를 작성"

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "{시스템 메시지}"},
            {"role": "user", "content": prompt},
        ]
    )
	
    """
    새로 찾은 셀렉터에서 불필요한 부분을 제거하고 new_xpath에 할당
    """
    new_xpath = response.choices[0].message.content.strip()
    new_xpath = re.sub(r'```[\s\S]*?```', '', new_xpath).strip()
    new_xpath = new_xpath.strip('"')
    
    print(f"new xpath: {new_xpath}")
    return new_xpath

 
현재는 API 자동화 테스트 작업 기간이지만 자가 치유 모듈에 욕심이 생기기도 하고, 이걸 해낸다면 API 자동화 테스트 작업에 온전히 집중할 수 있는 시간을 더 확보할 수 있을 것 같았습니다. 샘플 코드의 시연을 하며 팀장님과 계획에 대해 이야기를 나눈 결과 자가 치유 모듈 작업에 2일의 시간을 얻었습니다!


선작업

2일 중 첫번째 날에는 자가 치유 모듈을 위해 기존 코드를 개선했어요. 특별한 건 아니고 xpath나 css_selector를 개선했고 각 요소의 셀렉터에 디스크립션을 추가했습니다. 이것만 하더라도 거의 하루를 꼬박 보냈습니다. 수정하고 테스트하고의 무한 반복을 하루종일.. 이때부터 힘들다는 말을 입에 달기 시작했어요. 😂

새로 고친 셀렉터들 그리고 무한 테스트...


본작업

샘플 코드를 금새 만들어서 만만하게 봤던 본작업은 하루가 꼬박 걸렸습니다. 결국 처음 약속받은 2일을 모두 쓰게 되었네요. 샘플 코드는 구조화 되어있지 않고 한 페이지 내에서 데이터를 주고받았고, 테스트 할 요소도 하나 뿐이라 쉬웠지만 저희 UI 자동화 코드는 POM 구조를 기반으로 하고있어 페이지와 오브젝트, 테스트 conftest파일 등 테스트에 필요한 파일과 요소가 여기저기 흩어져 있어 샘플코드만큼 쉽지는 않았습니다. 그래도 구조화가 안되어 있었다면 이정도 코드 규모에서 자가 치유 모듈 붙이는건 그냥 포기할뻔 했습니다..^^
자가 치유 모델을 붙이는 걸로 끝나는 것이 아니고 GPT가 개입해서 새로운 셀렉터를 찾아 줬다는 것은 해당 요소에 문제가 있다는 뜻일겁니다. 따라서 GPT가 찾아준 셀렉터를 확인하기 위해 자가 치유 모듈이 새로운 셀렉터를 찾는데 성공하면 슬랙으로 노티를 발송하도록 했습니다.

이로써 어떤 요소에 문제가 있는지, GPT는 그 요소를 잘 찾아오고 있는지 알 수 있게 되었습니다. 이 코드는 테스트를 마쳐 운영을 시작했답니다 ㅎㅎㅎㅎ
작업이 완료되고 나서는 GPT가 찾아준 셀렉터를 코드에 바로 반영 해야할까? 라는 고민을 하게 되었습니다. 원티드에서는 Github에서 자동화 코드를 관리하고 있고 AWS의 인프라를 이용해 그곳에서 테스트 코드를 실행하고 있습니다. 만약 GPT가 찾아온 코드를 바로 서버에 올라간 코드에 반영해 버린다면 로컬, 리모트(Github), 서버의 코드가 서로 달라지는 문제가 생기게 됩니다. 그래서 일단 지금은 서버의 코드에 반영까지는 하지 않고 캐싱을 함으로써 동일한 요소에 문제가 생겼을 때 다시 GPT API를 호출하지 않도록 하는 방법으로 임시방편 해두었습니다.
서버에 코드 업데이트가 일어나면 Github에 Deploy하는 방법도 있겠지만 어디까지나 이번 자가 치유 모듈 작업은 UI 자동화 테스트에 드는 공수를 줄이는데 목적이 있으므로 여기까지만 하기로 했습니다.


마무리

이렇게 원티드의 UI 자동화 테스트는 GPT를 이용해 잘못된 부분이 있으면 스스로 고쳐가며 작업자의 수고를 조금이나마 덜어줄 수 있게 되었습니다. 제가 생각하는 이 자가 치유 모듈의 킥은 요소에 대한 디스크립션이라고 생각합니다. GPT에게 어떤 요소인지 잘 알 수 있도록 설명하는 디스크립션을 마련함으로써 찾아야 할 요소에 대한 정보를 더 명확하게 전달할 수 있었습니다.
다만 요소의 종류에 따라 잘 인식되는 것과 잘 인식되지 않는 것이 있어요. 명확한 기능을 가진(예를 들어 로그인 버튼) 경우 디스크립션을 "로그인 버튼" 이라고만 적어도 잘 인식하고 찾아오는 반면, 말로 뭐라 형용하기 힘든 형태나 기능의 요소는 새로운 셀렉터를 찾는데 어려워 하더라구요. 이런 경우에는 요소를 설명하는 스크립트를 이리 저리 바꿔가며 GPT는 어떻게 설명해야 잘 알아들을지 알아가는 과정이 필요할 것 같아요.(애초에 셀렉터를 인식하지 못하는 오류가 빈번한 것도 아니긴 합니다..ㅎㅎ)
간만에 또 OPENAI API라는 새로운 영역에 발을 들이고 유의미한 결과를 낼 수 있어서 재미있었던 2일이었습니다. 이제 다시 API로 넘어가야겠네요!
감사합니다.