UI自動化テストを実行する際、テストケースの安定性を維持するために明示的な待機処理を設定することがよくあります。明示的な待機とは、特定の要素の出現や特定の条件(クリック可能、表示など)が満たされるまで明確に待機することを指し、指定された時間内に条件が満たされない場合、Exceptionが発生します。
以下はSeleniumを使用したテストケースの例で、Seleniumにおける明示的な待機の使用方法を示しています。この例ではexpected_conditionsモジュールとWebDriverWaitクラスを使用しています。expected_conditionsはpyファイルの名前、つまりモジュール名であり、このモジュールには多くの条件クラスが含まれています。テストケースで使用されているtitle_isはその一つの条件クラスです。
WebDriverWaitはクラスで、このクラスの役割は特定の条件に基づいて、その条件が満たされているか継続的にチェックすることです。WebDriverWaitクラスにはuntil(条件が満たされるまで待機)とuntil_not(条件が満たされなくなるまで待機)の2つのメソッドしかありません。
class WebDriverWait(object):
def __init__(self, driver, timeout, poll_frequency=POLL_FREQUENCY, ignored_exceptions=None):
WebDriverWaitには4つのパラメータがあります:driver(ドライバ)、timeout(タイムアウト時間)、poll_frequency=POLL_FREQUENCY(ポーリング間隔、つまり条件が満たされているか判断する時間間隔、デフォルトは0.5秒)、ignored_exceptions=None(待機中に無視する例外、イテラブルな例外クラスのコレクション)。例えば、リストに[NoSuchElementException, NoSuchAttributeException, InvalidElementStateException....]のように設定できます。デフォルトでは、NoSuchElementExceptionのみを含むタプルです。要素が出現しないと条件が満たされているか判断できないため、継続的なポーリングプロセス中にNoSuchElementExceptionが発生するのは必然的であり、この例外を無視しないとプログラムが中断されます。
その中でdriverとtimeoutは必須の位置引数で、残りの2つはオプションのキーワード引数です。指定しない場合はデフォルト値が適用されます。
以下にSeleniumの待機条件についての議論に入ります。
待機条件
条件クラスの実装原理
selenium.webdriver.support.expected_conditionsモジュールにはすべての待機条件が含まれており、各条件クラスの構造は同じで、__init__コンストラクタメソッドと__call__メソッドがあります。
Pythonでは、クラスのオブジェクトを関数のように使用したい場合は、そのクラスに__call__メソッドを実装できます。例えば:
class TestCase:
def __init__(self):
self.driver = webdriver.Chrome(executable_path="./driver/chromedriver")
self.driver.get('http://www.baidu.com')
# sleep(2)
def __call__(self):
print(self.driver.title)
if __name__ == '__main__':
case = TestCase()
case()
case()オブジェクトの呼び出しは、__call__メソッド内のロジックを実行し、現在のページのタイトルを表示します。Seleniumの実装クラスを一つ取り上げます:
class presence_of_element_located(object):
def __init__(self, locator):
self.locator = locator
def __call__(self, driver):
return _find_element(driver, self.locator)
この条件クラスは、要素がページにレンダリングされているかどうかを判断します。この条件を使用する際は、まず要素の定位情報を渡してインスタンス化し、判断を行う際にはインスタンスオブジェクトを呼び出してdriverを渡します。インスタンスオブジェクトを呼び出すと、__call__メソッド内の条件判断ロジックが実行されます。
WebDriverWaitがどのように条件判断を行うか
記事の冒頭で使用した明示的な待機のコードを見てみましょう:
wait = WebDriverWait(self.driver, 2)
wait.until(EC.title_is('百度一下,你就知道'))
まずWebDriverWaitオブジェクトをインスタンス化し、その後untilメソッドを呼び出して条件のインスタンスオブジェクトを渡します。untilメソッド内では、条件が満たされているか継続的にポーリングします。
def until(self, method, message=''):
screen = None
stacktrace = None
end_time = time.time() + 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.time() > end_time:
break
raise TimeoutException(message, screen, stacktrace)
methodパラメータは渡された条件のインスタンスオブジェクトで、value = method(self._driver)の部分でオブジェクトの呼び出し、つまり__call__メソッド内のロジックの実行が行われます。
Seleniumが提供する条件一覧
- title_is:タイトルが出現しているか判断
- title_contains:ページタイトルに特定の文字列が含まれているか判断
- presence_of_element_located:特定の要素がDOMツリーに読み込まれているか判断(表示されているとは限らない)
- url_contains:現在のURLに特定のURLが含まれているか判断
- url_matches:現在のURLが特定の形式に一致するか判断
- url_to_be:現在のURLが出現しているか判断
- url_changes:現在のURLが変更されたか判断
- visibility_of_element_located:特定の要素がDOMツリーに追加され、かつ幅と高さが0より大きいか判断
- visibility_of:特定の要素が表示されているか判断
- presence_of_all_elements_located:少なくとも一つの要素がDOMツリーに存在するか判断し、見つかったすべての要素を返す
- visibility_of_any_elements_located:少なくとも一つの要素がページに表示されているか判断
- visibility_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:iframeに切り替え可能か判断
- invisibility_of_element_located:特定の要素がDOM中に表示されていないか判断
- element_to_be_clickable:特定の要素が表示されており、かつ有効(クリック可能)か判断
- staleness_of:特定の要素がDOMから削除されるのを待機
- element_to_be_selected:特定の要素が選択されているか判断(通常ドロップダウンリストで使用)
- element_located_to_be_selected:上記と同じ意味ですが、インスタンス化時に要素オブジェクトではなく定位情報を渡します
- element_selection_state_to_be:特定の要素の選択状態が期待通りか判断
- element_located_selection_state_to_be:上記と同じですが、渡される値が異なります
- number_of_windows_to_be:現在のウィンドウ数が期待値と等しいか判断
- new_window_is_opened:ウィンドウが追加されたか判断
- alert_is_present:ページにアラートが表示されているか判断
以上がSeleniumがサポートするすべての条件です。
カスタム条件の実装
多くの条件を説明しましたが、実際には独自の条件クラスを実装することもできます。
class page_is_load:
def __init__(self, expected_title, expected_url):
self.expected_title = expected_title
self.expected_url = expected_url
def __call__(self, driver):
is_title_correct = driver.title == self.expected_title
is_url_correct = driver.current_url == self.expected_url
return is_title_correct and is_url_correct
上記は独自に実装した条件クラスで、ページのURLとタイトルに基づいてページが正しく読み込まれたか判断します。
class TestCase:
def __init__(self):
self.driver = webdriver.Chrome(executable_path="./driver/chromedriver")
self.driver.get('http://www.baidu.com/')
# sleep(2)
def __call__(self):
print(self.driver.title)
def test_wait(self):
wait = WebDriverWait(self.driver, 2)
wait.until(page_is_load("百度一下,你就知道", "http://www.baidu.com/"))