가능하면 time.sleep()은 쓰지말고, WebDriverWait.until()을 사용하자.

Dahun Yoo·2023년 6월 1일
0

Lessons learned

목록 보기
7/13
post-thumbnail

가능하다면, Time.sleep() 대신에WebDriverWait.until() 을 사용해볼까요?

time.sleep()

time.sleep(secs)
Suspend execution of the calling thread for the given number of seconds. The argument may be a floating point number to indicate a more precise sleep time.

https://docs.python.org/3/library/time.html#time.sleep

우리는 종종 자동 테스트 코드를 작성할 때에 time.sleep() 을 걸곤 합니다. 대부분의 경우는 driver.find_element() 로 찾아내지 못하는 webElement를 식별하기 위해서인데요, 웹페이지 로딩이 아직 다 안되어서 찾고자 하는 element가 아직 로딩이 다 되지 않은 경우나, ajax를 이용한 비동기 처리에서 원하는 웹요소를 찾을 수 없을 때 사용합니다.

그러나 time.sleep() 은 위 설명에도 기재되어있듯, 실행되는 스레드 자체의 실행을 잠시 지연시키는 것으로, 과도하게 사용하면 전체 테스트 수행시간이 길어지게 됩니다.

네트워크나 데이터 양에 따라서는 빠르게 로딩되는 경우도 있을때에도 불필요한 정지를 하게되는 것입니다.

Wait

Selenium에서는 wait의 개념이 있습니다.

Selenium Webdriver provides two types of waits - implicit & explicit. An explicit wait makes WebDriver wait for a certain condition to occur before proceeding further with execution. An implicit wait makes WebDriver poll the DOM for a certain amount of time when trying to locate an element.

https://selenium-python.readthedocs.io/waits.html

implicity wait

이것은 최초 드라이버를 생성할 때, 설정해줍니다. 이 값을 설정해주면 전체 웹페이지 DOM에서 특정 element를 찾을 때까지 지정해준 시간동안 대기를 합니다.

driver.implicitly_wait(10)

모든 element를 찾을 때 사용할 수 있으며, DOM이 로딩될때까지 기다리므로 퍼포먼스 이슈가 있다고는 합니다.

explicity wait

이것은 명시적 대기라고 하는데, 특정 element를 찾을 때 조건을 설정해가면서 대기를 걸어줄 수 있습니다.

from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, 'someid')))

위 코드는 someid 라는 id값을 가진 element를, 클릭이 가능할 때까지 기다리는 일종의 조건문이라고도 할 수 있습니다. 대기시간은 10초를 설정하고 있으나, 해당 요소를 찾게되면 바로 다음 코드를 실행합니다.

Expected Condition은 아래의 추가적으로 아래의 조건들을 쓸 수 있습니다.

  • title_is
  • title_contains
  • presence_of_element_located
  • visibility_of_element_located
  • visibility_of
  • presence_of_all_elements_located
  • text_to_be_present_in_element
  • text_to_be_present_in_element_value
  • frame_to_be_available_and_switch_to_it
  • invisibility_of_element_located
  • element_to_be_clickable
  • staleness_of
  • element_to_be_selected
  • element_located_to_be_selected
  • element_selection_state_to_be
  • element_located_selection_state_to_be
  • alert_is_present

특정 Element를 식별 할 때, time.sleep()보다 wait.until()을 써야하는 이유

위에서 말씀드렸다시피, time.sleep() 은 전체 스레드를 실행대기시켜버립니다. 어떠한 하나의 버튼만 찾고싶은데 time.sleep() 을 걸어버리는 것은 불필요하고, 전체적인 테스트 실행 시간을 길어지게하는 원인이 됩니다.

WebDriverWait의 생성자와, until() 메소드 내부를 확인하면 아래와 같습니다.

 def __init__(
        self,
        driver,
        timeout: float,
        poll_frequency: float = POLL_FREQUENCY,
        ignored_exceptions: typing.Optional[WaitExcTypes] = None,
    ):
        """Constructor, takes a WebDriver instance and timeout in seconds.

        :Args:
         - driver - Instance of WebDriver (Ie, Firefox, Chrome or Remote)
         - timeout - Number of seconds before timing out
         - poll_frequency - sleep interval between calls
           By default, it is 0.5 second.
         - ignored_exceptions - iterable structure of exception classes ignored during calls.
           By default, it contains NoSuchElementException only.

        Example::

         from selenium.webdriver.support.wait import WebDriverWait \n
         element = WebDriverWait(driver, 10).until(lambda x: x.find_element(By.ID, "someId")) \n
         is_disappeared = WebDriverWait(driver, 30, 1, (ElementNotVisibleException)).\\ \n
                     until_not(lambda x: x.find_element(By.ID, "someId").is_displayed())
        """
        self._driver = driver
        self._timeout = float(timeout)
        self._poll = poll_frequency
        # avoid the divide by zero
        if self._poll == 0:
            self._poll = POLL_FREQUENCY
        exceptions = list(IGNORED_EXCEPTIONS)
        if ignored_exceptions:
            try:
                exceptions.extend(iter(ignored_exceptions))
            except TypeError:  # ignored_exceptions is not iterable
                exceptions.append(ignored_exceptions)
        self._ignored_exceptions = tuple(exceptions)
    def until(self, method, message: str = ""):
        """Calls the method provided with the driver as an argument until the \
        return value does not evaluate to ``False``.

        :param method: callable(WebDriver)
        :param message: optional message for :exc:`TimeoutException`
        :returns: the result of the last call to `method`
        :raises: :exc:`selenium.common.exceptions.TimeoutException` if timeout occurs
        """
        screen = None
        stacktrace = None

        end_time = time.monotonic() + self._timeout
        while True:
            try:
                value = method(self._driver)
                if value:
                    return value
            except self._ignored_exceptions as exc:
                screen = getattr(exc, "screen", None)
                stacktrace = getattr(exc, "stacktrace", None)
            time.sleep(self._poll)
            if time.monotonic() > end_time:
                break
        raise TimeoutException(message, screen, stacktrace)

until()while 문을 보시면, 일단 True로 무한 루프를 돌면서 특정값을 찾을 때까지 실행됩니다.

except에서 무시할 Exception으로 명시한 값에 대한 처리를 해주고, if 조건에서, 설정한 시간을 초과하는 경우에는 break 로 무한루프를 빠져나오고, TimeoutException 을 발생시킵니다. (언어가 다를 뿐, 다른 언어들의 selenium도 대부분 비슷한 동작입니다.)

즉, 찾고자하는 요소를 찾으면 설정한 시간을 다 채우지않고 바로 종료한다 는 것입니다.


잘 동작안할 때는, 사용하되 최소한으로만 사용하고 계속해서 해결책을 찾아보자.

이론적으로는 무조건 좋은데, 간혹가다가 wait는 동작안하는데, time.sleep() 을 주면 동작한다는 글을 심심치 않게 볼 수 있습니다.

이것은 wait.until() 에 주는 조건에 따라 driver가 판단하는 타이밍이 다를 수도 있고, 웹페이지의 DOM의 구조나 네트워크 이슈 등 다양한 이유에 따라서 못찾는 경우가 있습니다.

그렇다고 한 줄 한 줄 마다 sleep() 을 작성하진 마시고...

이럴때는 일단 time.sleep() 을 쓰되, 무분별한 사용은 지양하고, 점진적으로 대기시간을 조정해가며 실행가능한 최소한의 시간을 찾아내야합니다.

끝!!!!

profile
QA Engineer

0개의 댓글