Selenium で「要素が見つからない」「クリックできない」というエラーが頻発する原因の多くは、待機処理の問題です。time.sleep() で誤魔化していると Flaky Test になりがちです。WebDriverWait と expected_conditions を正しく使えば、安定したテストが書けます。
📌 この記事の対象読者
✅ time.sleep() を使っているが、本当はどうすれば良いか知りたい方 |
✅ NoSuchElementException や ElementNotInteractableException に悩んでいる方 |
✅ 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 Wait | WebDriverWait(詳細設定) | 🟡 上級者向け | ポーリング間隔・無視する例外を細かく設定 |
明示的待機(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_contains | URL に指定文字列が含まれる | ページ遷移の完了を待つ |
url_to_be | URL が指定の値と完全一致する | ログイン後の遷移先確認 |
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 クラスはなく、WebDriverWait の timeout・poll_frequency・ignored_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()
通常の
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 待機処理でよくある落とし穴
- presence_of_element_located で click しようとしてエラーになる
presence_of_element_locatedは DOM に存在するだけで、表示・有効状態は確認しません。クリックするにはelement_to_be_clickableを使いましょう。 - 暗黙的待機と明示的待機を混在させている
両方を設定すると待機時間が予期しない形で合算・競合します。どちらか一方に統一し、推奨は明示的待機(WebDriverWait)のみです。 - タイムアウト後に 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 - ローディング中の要素を取得して StaleElementReferenceException が出る
ページ更新や Ajax 後に再取得が必要な要素は、取得後すぐに古くなることがあります。staleness_ofで古い要素の消滅を待ってから再取得する、または毎回find_elementし直す設計にしましょう。 - アニメーション中の要素をクリックして ElementClickInterceptedException が出る
モーダルの表示アニメーションやツールチップが要素の上に被っているとクリックが失敗します。まずはロケーター・待機条件・オーバーレイ要素の有無を確認することが根本解決への近道です。ActionChainsや JavaScript クリック(driver.execute_script("arguments[0].click();", element))で回避できる場合もありますが、これらは問題を隠してしまうことがあるため最後の手段として検討しましょう。 - タイムアウト値を全部同じにしている
ページ遷移(5〜10秒)、API 応答(3〜5秒)、アニメーション(1〜2秒)など、待機内容によって適切なタイムアウト値は異なります。一律10秒などにすると遅いテストを量産することになります。 - driver.get() 後に何も待機せず要素を取得している
driver.get()は HTML の読み込み完了(document.readyState == "complete")まで待ちますが、JavaScript による動的なコンテンツ読み込みは待ちません。ページ遷移後に要素を操作するときもWebDriverWaitを使いましょう。
よくある質問(FAQ)
デフォルトのポーリング間隔は 0.5秒です。つまり条件が満たされているかを0.5秒ごとにチェックします。この間隔は poll_frequency パラメータで変更できます(例:WebDriverWait(driver, 10, poll_frequency=0.2))。チェック頻度を上げると応答性が向上しますが、ブラウザへの負荷も増えます。
pytest fixture に組み込む方法が最もシンプルです(本記事の「pytest fixture との組み合わせ」参照)。
さらに大規模なプロジェクトでは Page Object Model(POM) の BasePage クラスに wait を持たせて全ページで共通利用するパターンが一般的です。タイムアウト値は conftest.py で定数管理すると変更が容易になります。
一般的な目安は 通常の待機で5〜10秒、ネットワークが絡む待機で10〜15秒です。CI 環境はローカルより遅いことが多いため、余裕を持たせましょう。ただし長くするほどテストが遅くなるため、実際の応答時間を計測して適切な値を設定することが大切です。
基本的には避けてください。 しかし、例外としてアニメーションの完了待ちなど「一定時間かかることが確実で、条件での判定が難しいケース」では短い sleep(例:0.3秒)を使う実務的な判断もあります。ただしこれは最後の手段で、まず expected_conditions やカスタム条件で解決できないか検討しましょう。
until(条件) は条件が True になるまで待ちます。until_not(条件) は条件が False になるまで待ちます。ローディングスピナーが消えるのを待つときは until(EC.invisibility_of_element_located(...))(または until_not(EC.visibility_of_element_located(...)))を使います。
Playwright は自動待機(Auto-waiting)が標準搭載されているため、Selenium のような明示的な待機設定が不要なケースがほとんどです。page.click()・page.fill() などの操作は、要素が操作可能になるまで自動で待機します。ただし特定の条件を待つ場合は page.wait_for_selector()・page.wait_for_url() などが使えます。
NoSuchElementException は find_element を呼んだ瞬間に要素が見つからないときに発生します。TimeoutException は WebDriverWait の待機条件がタイムアウト時間内に満たされなかったときに発生します。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_clickable や visibility_of_element_located に置き換えるところから始めてみましょう。
実務経験上、原因不明の Flaky Test をトレースすると、その多くが待機が足りない・条件が間違っている・ロケーターが壊れているのいずれかです。まず待機処理を見直すことが Flaky Test 解消の最短ルートです。
GitHub Actions・GitLab CI などの CI 環境はローカルマシンより処理が遅いことがあります。ローカルで問題なく通るテストが CI で
TimeoutException になる場合、タイムアウト値を少し長くする(例:10秒 → 15秒)か、CI 専用の設定ファイルで調整する方法が有効です。Selenium vs Playwright:待機処理の考え方の違い
| 比較項目 | Selenium | Playwright |
|---|---|---|
| 待機の基本思想 | 明示的待機中心(自分で書く) | Auto-waiting 中心(標準搭載) |
| デフォルト動作 | 待機なし(即時検索) | 操作前に自動で待機 |
| time.sleep() 依存リスク | △ 書きがち | ○ 少ない |
| カスタム待機 | WebDriverWait + EC / lambda | expect() / wait_for_selector() |
Playwright入門|Pythonでブラウザ自動化を始める手順を初心者向けに解説
