Selenium webdriver - test automation

2023. 1. 24. 22:59Devops

Selenium

근래에 웹테스트 자동화를 위한 방안을 모색하던 중 Selenium을 접하게 되었습니다.
Selenium을 설명하자면 일종의 Platform과도 같은 느낌이 있는 것 같습니다. 참고사이트

Selenium IDE, Selenium Webdriver, Selenium Grid...

처음엔 IDE로 이를 접해보았습니다.
IDE는 Chrome, Firefox 등에서 Plugin으로 설치가 가능하며 Recording 기능을 이용하면 손쉽게 Macro과 같은 형태로 반복 작업에 필요한 시나리오를 만드는 것이 가능합니다. 결과물을 Code로 export하는 것도 가능하며, 완성된 Project 파일을 command line으로 실행시켜 줄 수 있는 selenium-side-runner도 지원합니다.

Webdriver의 경우 각 Browser별로 지원(링크)하며 (이를 통합관리 해줄 수 있는 WebDriverManager도 존재합니다.)

실제 Browser를 UI레벨에서 코드 레벨에서 제어가 가능합니다.

Webdriver를 Google에서 검색해보면 주로 사용처가 웹크롤링, 자동화 테스트등에서 주로 사용됨을 확인할 수 있는 데, 나의 경우 Webpage 개발 후 자동화 테스트에 이용하기 위한 목적으로 발을 담그게 되었습니다.


Webdriver

처음 Webdriver의 사용 코드를 예제로 접했을 때, 직관적으로 이를 어떻게 사용하면 될지 바로 감이 올 정도로 쉬워보였습니다.

지금부터 사용할 예제는 모두 python으로 작성되었습니다. Webdriver 자체가
python, java, javascript등 많은 언어를 지원하지만 내가 python을 가볍게 쓰는 용도로 즐겨쓰는 이유 입니다.

driver = webdriver.Chrome()
driver.get("https://www.test.com")
driver.find_element(By.ID, "test_button").click()
driver.find_element(By.ID, "test_input").send_keys("test")

그냥 Chrome용 webdriver를 생성하고, 원하는 웹페이지를 불러온 다음, 원하는 버튼을 찾아서 클릭!

실제로 첫 코드를 작성해서 돌려보기 전까지는 앞으로 어떤 고생을 하게 될지 전혀 몰랐으니...

Webdriver는 실제 Network 환경에서 Browser를 시뮬레이션 하는 형태로 동작하는 것에 가깝습니다. 그러다보니, Network 환경 혹은 Client측의 상황에 따라서 각종 타이밍 이슈등이 발생하기 쉽상입니다. 그러다보니 동일한 코드임에도 불구하고 실패했다가... 성공했다가... 합니다.

이런 코드를 자동화에 사용할 수는 없었습니다. 에러가 났을 때, 이 에러가 정말 내가 작성한 시나리오 과정에서 발생한 오류인지, Browser 동작 과정에서 발생한 타이밍 적인 상황 때문에 시나리오가 진행되다가 먹통이 된건지 확인해 봐야하는 것도 낭비이기 때문입니다.

이를 보정하기 위해 여러가지 방책을 고민했고, 이를 해결하기 위한 Wrapper를 만들기에 이르렀습니다.
아래에 소개될 Wrapper는 정답으로 볼 형태가 아니라, 개인적으로 풀어나간 노하우에 가까운 Logic처리 임을 감안하고 봐주시면 좋겠습니다.

 

Webdriver wrapper

Driver wrapper

class TestDriverEngine(unittest.TestCase):
    def __init__(self, driver):
        self._driver = driver
        print("Test Driver init")

    def get(self, url):
        print('Get | {} '.format(url))
        return self._driver.get(url)

    def set_window_size(self, width, height):
        print('Set window size | {} / {} '.format(width, height))
        return self._driver.set_window_size(width, height)

    def find_element(self, by_type, param):
        print('Find | {} : {}'.format(by_type, param))
        WebDriverWait(self._driver, 10000).until(expected_conditions.presence_of_all_elements_located((by_type, param)))
        WebDriverWait(self._driver, 10000).until(expected_conditions.visibility_of_element_located((by_type, param)))
        return TestElement(self._driver, by_type, param)

    def quit(self):
        return self._driver.quit()

    def close(self):
        return self._driver.close()

    def getDriver(self):
        return self._driver

먼저 Driver wrapper는 find_element 시점에 공통적인 사전 점검을 general한 방식으로 통일 하기 위함입니다.
찾고자 하는 Element가 rendering후 positioning이 완료 되었는 지, 유저의 관점 테스트를 하다보니 실제 Action을 취할 Element들은 당연히 Visible한 상태가 되어어야 하기에 해당 스테이트를 확인하는 작업이 find_element의 선 작업으로 들어가 있습니다.

그 외에 Method들은 단순히 내부 Driver의 호출이나, 공통적인 Log를 찍어주기 위한 용도입니다.
Find의 결과로 return되는 element 또한 Wrapper로 구성하였습니다.

 

Element wrapper

class TestElement():
    def __init__(self, driver, by_type, param):
        self._driver = driver
        self._by_type = by_type
        self._param = param

    def click(self, withlog=True):
        while True:
            try:
                element = self._driver.find_element(self._by_type, self._param)
                WebDriverWait(self._driver, 10000).until(expected_conditions.visibility_of(element))
                if withlog:
                    text = element.text
                    if len(text) > 10:
                        text = text[0:9]
                    print('Click | {} : {}({})'.format(self._by_type, self._param, text))
                element.click()
                time.sleep(BASE_WAITING)
                return;
            except (NoSuchElementException, ElementClickInterceptedException, StaleElementReferenceException) as e:
                if withlog:
                    print(e)
                    print("retry...")
                time.sleep(BASE_WAITING)
                continue

    def send_keys(self, keys):
        while True:
            try:
                element = self._driver.find_element(self._by_type, self._param)
                WebDriverWait(self._driver, 10000).until(expected_conditions.element_to_be_clickable((self._by_type, self._param)))
                self.click(withlog=False)
                dp = "****"
                element_type = element.get_attribute("type"); 
                if element_type != "password":
                    dp = keys

                print('Send key | {} : {} ({})'.format(self._by_type, self._param, dp))
                element.send_keys(keys)
                return;
            except (NoSuchElementException, StaleElementReferenceException) as e:
                print(e)
                print("retry...")
                time.sleep(BASE_WAITING)
                continue


    def find_element(self, by_type, param):
        print('Find | {} : {}'.format(by_type, param))
        WebDriverWait(self._driver, 10000).until(expected_conditions.presence_of_all_elements_located((by_type, param)))
        WebDriverWait(self._driver, 10000).until(expected_conditions.visibility_of_element_located((by_type, param)))
        self._driver.find_element(self._by_type, self._param).find_element(by_type, param)

    def get(self):
        return self._driver.find_element(self._by_type, self._param)

실제 웹페이지에서 유저가 주로 하게될 주요 Action은 input박스에 값을 입력하거나, Button 클릭임으로 대표적인 처리를 예제로 구성하였습니다. Rendering 중 Click event가 발생할 경우 오류가 발생할 수 있기 때문에 Retry logic도 공통적으로 처리하기 위해 Wrapper안에 두었습니다. input박스에 값을 입력하는 경우 User의 Action과 동일하게 Click을 먼저 하고, 입력하도록 하였습니다.

driver = TestDriverEngine(webdriver.Chrome())

driver.get("https://www.test.com")
driver.find_element(By.ID, "test_button").click()
driver.find_element(By.ID, "test_input").send_keys("test")

실제 구성한 구조를 Class diagram으로 나타내는 위와 같은 형태로 나타낼 수 있습니다.
사실 TestElement는 자체 Element를 갖고있는 구조가 아니라, 필요할 때 Driver로 부터 갖고오는 형태로 구성되어져 있습니다.

이는 Element를 얻어오는 과정에서 발생하는 오류들을 가급적 피하기 위해 실제로 필요한 순간으로 미루기 위함이었습니다.

위와 같이 Warpper를 구성한 후에는 최초 작성한 Testcase가 잘동작 됨을 확인할 수 있었습니다.

이제는 가벼운 마음으로 심플하게 유저의 엑션 단위로 라인을 추가해가며 쉽게 Testcase를 확장할 수 있게 되었습니다.

'Devops' 카테고리의 다른 글