Selenium WebDriverWait完全ガイド|待機処理を正しく理解してFlaky Testを解消する

Selenium で「要素が見つからない」「クリックできない」というエラーが頻発する原因の多くは、待機処理の問題です。time.sleep() で誤魔化していると Flaky Test になりがちです。WebDriverWaitexpected_conditions を正しく使えば、安定したテストが書けます。

📌 この記事の対象読者

time.sleep() を使っているが、本当はどうすれば良いか知りたい方
NoSuchElementExceptionElementNotInteractableException に悩んでいる方
WebDriverWait は知っているが、expected_conditions の使い方を整理したい方
✅ テストが環境によって成功したり失敗したりする Flaky Test を解消したい方

📖 この記事を読むとできること

✔ 暗黙的待機・明示的待機・Fluent Wait の違いと使い分けが分かる
expected_conditions の主要メソッドを場面別に使い分けられる
time.sleep() を排除して安定したテストを書けるようになる
✔ カスタム待機条件を自分で作れるようになる

筆者は複数のプロジェクトで Selenium を使ったブラウザ自動化テストを実践してきました。
待機処理の問題は Flaky Test の原因として最も多く遭遇するケースの一つです。
本記事はその経験をもとに、実務で使える待機処理パターンを体系的にまとめたものです。

✅ この記事の結論

  • time.sleep() は禁止。代わりに WebDriverWait + expected_conditions を使う
  • 暗黙的待機と明示的待機は混在させない
  • 待機条件は「何を待つか」に合わせて expected_conditions を選ぶ

なぜ Selenium で待機処理が必要なのか

現代の Web アプリケーションは非同期処理(Ajax・JavaScript)で動いています。ページを開いた瞬間には要素がまだ存在しないことがほとんどです。

よくある状況発生するエラー
ページ読み込み中に要素を取得しようとしたNoSuchElementException
要素は存在するが非表示・無効化されているElementNotInteractableException
他の要素がアニメーション中で上に被っているElementClickInterceptedException
要素が古い DOM から切り離されたStaleElementReferenceException

これらのエラーを回避するために「要素が準備できるまで待つ」処理が必要です。

time.sleep() がダメな理由

import time
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com/login")  # サンプルURL
time.sleep(3)  # ← これがダメ
driver.find_element(By.ID, "email").send_keys("user@example.com")
問題点具体的な影響
無駄な待機時間が発生する要素が0.5秒で表示されても3秒待つ→CI が遅くなる
環境によっては足りなくなるCI サーバーが遅い日は3秒でも足りずテストが失敗する
Flaky Test の温床になるネットワーク速度により成功・失敗が変わる
根本解決にならないsleep を長くするほど遅くなるだけで、問題の本質は変わらない

Selenium の3種類の待機方法

種類設定方法推奨度用途
明示的待機WebDriverWait🔴 推奨特定の条件が満たされるまで待つ
暗黙的待機implicitly_wait()⚠️ 限定的要素の DOM 出現をグローバルに待つ
Fluent WaitWebDriverWait(詳細設定)🟡 上級者向けポーリング間隔・無視する例外を細かく設定

明示的待機(WebDriverWait)の使い方

明示的待機は特定の条件が満たされるまで最大○秒待つという書き方です。条件が満たされれば即座に処理を続けるため、無駄な待機が発生しません。

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

driver = webdriver.Chrome()
driver.get("https://example.com/login")  # サンプルURL

# WebDriverWait の基本形
# WebDriverWait(driver, タイムアウト秒数).until(条件)
wait = WebDriverWait(driver, 10)  # 最大10秒待つ

# 要素が表示されるまで待つ(入力欄など)
email_input = wait.until(
    EC.visibility_of_element_located((By.ID, "email"))
)
email_input.send_keys("user@example.com")

# 要素が表示されてクリック可能になるまで待つ
submit_btn = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-button']"))
)
submit_btn.click()

expected_conditions の主要メソッド一覧

メソッド待機する条件使用場面
presence_of_element_located要素が DOM に存在する(非表示でもOK)テキストや値を取得したいとき
visibility_of_element_located要素が表示されている(サイズ > 0)表示確認・スクリーンショット
element_to_be_clickable要素が表示されていてかつ有効(クリック可)ボタンをクリックするとき(最もよく使う)
invisibility_of_element_located要素が非表示になったローディングスピナーが消えるのを待つ
text_to_be_present_in_element要素のテキストに指定文字列が含まれる「保存しました」などの完了メッセージ待ち
url_containsURL に指定文字列が含まれるページ遷移の完了を待つ
url_to_beURL が指定の値と完全一致するログイン後の遷移先確認
presence_of_all_elements_located指定ロケーターに一致する要素が1つ以上あるリスト・テーブルの読み込み待ち
staleness_of要素が DOM から切り離された(stale になった)ページ再読み込み後の要素更新待ち
alert_is_presentアラートダイアログが表示されたconfirm・alert の処理前

場面別のコード例

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

wait = WebDriverWait(driver, 10)

# ① ボタンをクリックする(最もよく使う)
btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='submit-btn']")))
btn.click()

# ② ローディングスピナーが消えるのを待つ
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".loading-spinner")))

# ③ 完了メッセージが表示されるのを待つ
wait.until(EC.text_to_be_present_in_element(
    (By.CSS_SELECTOR, "[data-testid='status-message']"),
    "保存しました"
))

# ④ ページ遷移を待つ
wait.until(EC.url_contains("/dashboard"))

# ⑤ リストが読み込まれるのを待つ(1件以上)
items = wait.until(
    EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".item-list li"))
)
assert len(items) > 0

# ⑥ アラートを処理する
wait.until(EC.alert_is_present())
alert = driver.switch_to.alert
alert.accept()

暗黙的待機(implicitly_wait)とその注意点

暗黙的待機はすべての find_element にグローバルなタイムアウトを設定します。一度設定すれば全操作に自動で適用されます。

driver = webdriver.Chrome()
driver.implicitly_wait(10)  # 全 find_element で最大10秒待つ
driver.get("https://example.com/login")  # サンプルURL

# これ以降の find_element は最大10秒待つ
email_input = driver.find_element(By.ID, "email")
⚠️ 暗黙的待機と明示的待機は混在させない
両方を同時に使うと予期しない動作になることがあります(待機時間が合算される・競合するなど)。
大規模プロジェクトでは明示的待機(WebDriverWait)へ統一するケースが多いです。暗黙的待機と明示的待機は同時に使うと予期しない動作になることがあるため、どちらか一方に統一しましょう。

WebDriverWait の詳細設定(いわゆる Fluent Wait 的な使い方)

Fluent Wait は WebDriverWait の詳細設定版で、ポーリング間隔と無視する例外を細かく指定できます。
なお、Selenium Python には Java のような独立した FluentWait クラスはなく、WebDriverWaittimeoutpoll_frequencyignored_exceptions を調整することでいわゆる Fluent Wait 的な動作を実現します。

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException

# Fluent Wait の設定
wait = WebDriverWait(
    driver,
    timeout=15,             # 最大待機時間(秒)
    poll_frequency=0.5,     # チェック間隔(秒)。デフォルトは0.5秒
    ignored_exceptions=[    # 無視する例外(この例外が出ても待ち続ける)
        NoSuchElementException,
        StaleElementReferenceException,
    ]
)

element = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='dynamic-btn']"))
)
element.click()
💡 Fluent Wait はどんなときに使うか
通常の WebDriverWait で十分なケースがほとんどです。Fluent Wait が役立つのは以下のような場面です。

  • ポーリング間隔を細かく調整して応答性を上げたいとき
  • StaleElementReferenceException が頻発する動的なページで待ちたいとき
  • 待機中に特定の例外を無視したいとき

カスタム待機条件を作る

expected_conditions にない条件を待ちたいときは、カスタム条件を関数またはクラスで定義できます。

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

# ① lambda を使ったシンプルな書き方
wait = WebDriverWait(driver, 10)

# 要素の属性値が変わるのを待つ
wait.until(lambda d: d.find_element(By.ID, "status").get_attribute("data-state") == "loaded")

# ② クラスを使った再利用可能な書き方
class element_has_class:
    """指定した要素が特定の CSS クラスを持つまで待つ"""
    def __init__(self, locator: tuple, css_class: str) -> None:
        self.locator = locator
        self.css_class = css_class

    def __call__(self, driver):
        element = driver.find_element(*self.locator)
        return self.css_class in element.get_attribute("class")


# 使用例:ボタンが "active" クラスを持つまで待つ
wait.until(element_has_class(
    (By.CSS_SELECTOR, "[data-testid='submit-btn']"),
    "active"
))

pytest fixture との組み合わせ

実務では WebDriverWait を pytest fixture に組み込むことで、テストコードをシンプルに保てます。

# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait


@pytest.fixture
def driver():
    d = webdriver.Chrome()
    d.implicitly_wait(0)   # 暗黙的待機は無効化しておく
    yield d
    d.quit()


@pytest.fixture
def wait(driver):
    return WebDriverWait(driver, 10)
# test_login.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC


def test_login_success(driver, wait) -> None:
    driver.get("https://example.com/login")  # サンプルURL

    wait.until(EC.visibility_of_element_located((By.ID, "email"))).send_keys("user@example.com")
    wait.until(EC.visibility_of_element_located((By.ID, "password"))).send_keys("password123")
    wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='login-btn']"))).click()

    wait.until(EC.url_contains("/dashboard"))
    assert "/dashboard" in driver.current_url

待機処理ではまりやすいポイント 7選

⚠️ Selenium 待機処理でよくある落とし穴

  1. presence_of_element_located で click しようとしてエラーになる
    presence_of_element_located は DOM に存在するだけで、表示・有効状態は確認しません。クリックするには element_to_be_clickable を使いましょう。
  2. 暗黙的待機と明示的待機を混在させている
    両方を設定すると待機時間が予期しない形で合算・競合します。どちらか一方に統一し、推奨は明示的待機(WebDriverWait)のみです。
  3. タイムアウト後に TimeoutException が発生して気づかない
    WebDriverWait は条件が満たされない場合 TimeoutException を発生させます。テストが落ちたとき、エラーメッセージを読んで「何を待っていたか」を確認しましょう。スクリーンショットを撮って状態を記録するのも有効です。

    from selenium.common.exceptions import TimeoutException
    try:
        wait.until(EC.element_to_be_clickable((By.ID, "submit")))
    except TimeoutException:
        driver.save_screenshot("timeout_debug.png")
        raise
  4. ローディング中の要素を取得して StaleElementReferenceException が出る
    ページ更新や Ajax 後に再取得が必要な要素は、取得後すぐに古くなることがあります。staleness_of で古い要素の消滅を待ってから再取得する、または毎回 find_element し直す設計にしましょう。
  5. アニメーション中の要素をクリックして ElementClickInterceptedException が出る
    モーダルの表示アニメーションやツールチップが要素の上に被っているとクリックが失敗します。まずはロケーター・待機条件・オーバーレイ要素の有無を確認することが根本解決への近道です。ActionChains や JavaScript クリック(driver.execute_script("arguments[0].click();", element))で回避できる場合もありますが、これらは問題を隠してしまうことがあるため最後の手段として検討しましょう。
  6. タイムアウト値を全部同じにしている
    ページ遷移(5〜10秒)、API 応答(3〜5秒)、アニメーション(1〜2秒)など、待機内容によって適切なタイムアウト値は異なります。一律10秒などにすると遅いテストを量産することになります。
  7. driver.get() 後に何も待機せず要素を取得している
    driver.get() は HTML の読み込み完了(document.readyState == "complete")まで待ちますが、JavaScript による動的なコンテンツ読み込みは待ちません。ページ遷移後に要素を操作するときも WebDriverWait を使いましょう。

よくある質問(FAQ)

Q. WebDriverWait のデフォルトのポーリング間隔は?

デフォルトのポーリング間隔は 0.5秒です。つまり条件が満たされているかを0.5秒ごとにチェックします。この間隔は poll_frequency パラメータで変更できます(例:WebDriverWait(driver, 10, poll_frequency=0.2))。チェック頻度を上げると応答性が向上しますが、ブラウザへの負荷も増えます。

Q. WebDriverWait を複数テストで共通化する方法は?

pytest fixture に組み込む方法が最もシンプルです(本記事の「pytest fixture との組み合わせ」参照)。
さらに大規模なプロジェクトでは Page Object Model(POM) の BasePage クラスに wait を持たせて全ページで共通利用するパターンが一般的です。タイムアウト値は conftest.py で定数管理すると変更が容易になります。

Q. タイムアウト値は何秒にすればいいですか?

一般的な目安は 通常の待機で5〜10秒、ネットワークが絡む待機で10〜15秒です。CI 環境はローカルより遅いことが多いため、余裕を持たせましょう。ただし長くするほどテストが遅くなるため、実際の応答時間を計測して適切な値を設定することが大切です。

Q. WebDriverWait と time.sleep を併用してもいいですか?

基本的には避けてください。 しかし、例外としてアニメーションの完了待ちなど「一定時間かかることが確実で、条件での判定が難しいケース」では短い sleep(例:0.3秒)を使う実務的な判断もあります。ただしこれは最後の手段で、まず expected_conditions やカスタム条件で解決できないか検討しましょう。

Q. until と until_not の違いは何ですか?

until(条件) は条件が True になるまで待ちます。until_not(条件) は条件が False になるまで待ちます。ローディングスピナーが消えるのを待つときは until(EC.invisibility_of_element_located(...))(または until_not(EC.visibility_of_element_located(...)))を使います。

Q. Playwright にも同じような待機処理が必要ですか?

Playwright は自動待機(Auto-waiting)が標準搭載されているため、Selenium のような明示的な待機設定が不要なケースがほとんどです。page.click()page.fill() などの操作は、要素が操作可能になるまで自動で待機します。ただし特定の条件を待つ場合は page.wait_for_selector()page.wait_for_url() などが使えます。

Q. NoSuchElementException と TimeoutException の違いは?

NoSuchElementExceptionfind_element を呼んだ瞬間に要素が見つからないときに発生します。TimeoutExceptionWebDriverWait の待機条件がタイムアウト時間内に満たされなかったときに発生します。Flaky Test では多くの場合 TimeoutException が出るため、待機条件や時間を見直しましょう。

まとめ

ポイント内容
time.sleep() は使わない無駄な待機・Flaky Test の原因になる
明示的待機を使うWebDriverWait(driver, 10).until(条件) が基本形
EC を場面で使い分けるクリック→element_to_be_clickable、消滅待ち→invisibility_of_element_located
混在させない暗黙的待機と明示的待機は同時に使わない
WebDriverWait 詳細設定ポーリング間隔・無視する例外を細かく設定したいときに使う(いわゆる Fluent Wait 的な使い方)
カスタム条件も作れるlambda またはクラスで独自の待機条件を定義できる
fixture に組み込むWebDriverWait を pytest fixture にして各テストで再利用する

待機処理を正しく書くことは、Selenium テストの安定性を上げる最も効果的な方法の一つです。
まず手持ちのテストコードで time.sleep() を検索し、element_to_be_clickablevisibility_of_element_located に置き換えるところから始めてみましょう。

💡 Flaky Test の7〜8割は待機処理かロケーターの問題
実務経験上、原因不明の Flaky Test をトレースすると、その多くが待機が足りない・条件が間違っている・ロケーターが壊れているのいずれかです。まず待機処理を見直すことが Flaky Test 解消の最短ルートです。
💡 CI 環境ではタイムアウトを長めに設定する
GitHub Actions・GitLab CI などの CI 環境はローカルマシンより処理が遅いことがあります。ローカルで問題なく通るテストが CI で TimeoutException になる場合、タイムアウト値を少し長くする(例:10秒 → 15秒)か、CI 専用の設定ファイルで調整する方法が有効です。

Selenium vs Playwright:待機処理の考え方の違い

比較項目SeleniumPlaywright
待機の基本思想明示的待機中心(自分で書く)Auto-waiting 中心(標準搭載)
デフォルト動作待機なし(即時検索)操作前に自動で待機
time.sleep() 依存リスク△ 書きがち○ 少ない
カスタム待機WebDriverWait + EC / lambdaexpect() / wait_for_selector()
📎 Playwright の自動待機について詳しく知りたい方は
Playwright入門|Pythonでブラウザ自動化を始める手順を初心者向けに解説
タイトルとURLをコピーしました