Seleniumテスト大量実行で発生するWinError 10061を解消する方法

複数のSeleniumテストを一斉に実行すると、途中で意図しないエラーが発生してしまうことはありませんか?特に「Failed to establish a new connection: [WinError 10061]」が突然現れると原因究明に頭を悩ませる方も多いようです。本記事では、その対策を具体的にご紹介します。

大量バッチ実行時に発生するSeleniumエラーの概要

テストを自動化する上で、SeleniumとPythonを組み合わせたワークフローは非常に便利です。しかし、Jenkinsを使ったパイプラインやpytestの並列実行で一度に20~30件のテストを回そうとすると、以下のような問題が発生するケースがあります。

Failed to establish a new connection: [WinError 10061]とは?

エラー文にある「No connection could be made because the target machine actively refused it」というメッセージは、ネットワークやポートの問題で接続が拒否されたことを意味します。Windows環境では、エフェメラルポート(動的に割り当てられる一時的なポート)の枯渇や、同時に起動したChromeDriverの競合などが原因でこのエラーが頻出する場合があります。

同時発生の現象:ChromeDriverのクラッシュ

AWSなどのリモート環境でWindows Serverを使用している場合、テスト中にChromeDriver.exeがクラッシュしてしまうケースも見受けられます。1件ずつテストを実行すると問題ないのに、まとめて実行すると途端にドライバが落ちてしまうことがあるのです。これは高負荷時のリソース不足や、バージョンミスマッチによる不具合も要因として考えられます。

原因と考えられる主な要素

本節では、大量のテストを実行する際にエラーが起きる原因を深掘りしていきます。単にドライバのバージョンを合わせるだけでは解決しない場合もあるので、複合的にチェックしてみましょう。

1. ChromeとChromeDriverのバージョン不整合

ChromeとChromeDriverのバージョンが一致していないと、テストが不安定になる要因となります。特にWindows Server上では、Chromeの自動更新が停止していることや、ChromeDriverを手動更新していないことが原因で、バージョンに差異が生じやすいです。

項目確認ポイント
Chromeブラウザ最新バージョンがインストールされているか
ChromeDriverブラウザと一致したバージョンか

バージョンが一致しないと、一部のAPIのサポートが欠落していたり、挙動が変化しているためにテストが失敗しやすくなります。特に大量並列実行時は、負荷が高まる分、些細なバージョン差による不具合が顕在化しやすいので要注意です。

2. サーバーのリソース不足や負荷集中

AWSを含め、Windows ServerはCPUやメモリ、ディスクI/Oなどのリソースが限られています。テスト実行時に以下のような症状が現れる場合、リソース不足を疑いましょう。

  • CPU使用率が常に高止まりしている
  • メモリの使用率がほぼ100%近くまで上昇している
  • スワップ領域を多用してディスクアクセスが増えている

負荷がかかりすぎると、ChromeDriverやブラウザの起動が遅延し、結果的にポートの接続処理がタイムアウトして「Failed to establish a new connection…」というエラーにつながることがあります。

エフェメラルポートの枯渇

Windowsでは一時的に割り当てられるポートの範囲が決まっており、大量のプロセスを同時に立ち上げるとポートの枯渇が起きる可能性があります。この時、必要なポートを確保できずに接続が拒否され、WinError 10061が発生することがあるのです。

Windowsで現在のエフェメラルポート範囲を確認する方法の一例:

netsh int ipv4 show dynamicportrange tcp

範囲が狭い場合は、以下のように設定してポートを拡張することも検討できます(再起動などが必要になる場合があるので注意)。

netsh int ipv4 set dynamicportrange tcp start=10000 num=55535

※ただしポート設定変更の影響範囲は慎重に判断しましょう。

3. Jenkinsやpytestの並列実行設定の見直し

並列実行のメリットはテストの短時間化ですが、過度にスレッドやプロセスを増やしすぎるとリソース面やポート面で衝突を起こします。

  • Jenkinsのジョブ数やスレーブの並列度を必要以上に増やしていないか
  • pytestの-nオプション(並列実行数)が高すぎないか

適切な並列数を見極めるには、まずは少ない並列度(例:2~4程度)でテストが安定して動くかを確認し、徐々に並列数を増やしていくのがおすすめです。

一時ファイルやプロセスの後片付け不足

テスト完了後にChromeDriverなどのプロセスが残存している場合、次回テスト開始時にリソース競合やポート競合を引き起こすことがあります。定期的にサーバー再起動やプロセスの強制終了を行う、あるいはテスト終了時に手動でプロセスをクリーンアップするスクリプトを組むと良いでしょう。

具体的な解決策・対処法

ここからは、上記の原因を踏まえた対策について詳しく解説していきます。状況に合わせて複合的に導入すると、より安定したテスト環境が構築できます。

1. ChromeブラウザとChromeDriverのバージョンを合わせる

まずは基本ですが、意外と見落とされがちなポイントです。常に最新版に揃えるのが理想ですが、業務上の都合でChromeを固定バージョンにしたい場合は、それに対応するChromeDriverも固定しておく必要があります。

実装例:ChromeDriverのバージョン固定

Pythonを使ってバージョンを明示的に指定し、ChromeDriverをダウンロードするスクリプト例です。CI環境(Jenkinsなど)で実行することで、常に特定のバージョンが使用されるように管理できます。

#!/usr/bin/env python
import os
import requests
import zipfile
import io

CHROME_DRIVER_VERSION = "109.0.5414.74"  # 例:必要なバージョンを指定

def download_chromedriver(version):
    url = f"https://chromedriver.storage.googleapis.com/{version}/chromedriver_win32.zip"
    response = requests.get(url)
    z = zipfile.ZipFile(io.BytesIO(response.content))
    z.extractall("chromedriver")

if __name__ == "__main__":
    if not os.path.exists("chromedriver/chromedriver.exe"):
        print("Downloading ChromeDriver...")
        download_chromedriver(CHROME_DRIVER_VERSION)
        print("Downloaded and extracted ChromeDriver.")
    else:
        print("ChromeDriver already exists.")

このように、バージョン管理をスクリプト化しておくと、意図しないバージョンアップやダウングレードを避けられるため、テストの安定性が向上します。

2. サーバースペックの強化や負荷分散

CPUコア数やメモリ量が十分でない場合、大量のブラウザインスタンスが同時に起動するだけでリソースを圧迫します。AWSのEC2インスタンスタイプをワンランク上げたり、別のサーバーにテストを分散する方法も検討材料となります。

  • EC2インスタンスのタイプ: t2.medium → t2.large / t3.largeなど
  • 一台のサーバーに詰め込みすぎない: テストジョブを複数台に振り分ける

また、テスト時間をずらして実行するスケジュール管理も有効です。一斉にすべてを走らせず、タイムラグを持たせて実行するとエフェメラルポートやリソース不足の回避につながります。

3. ChromeDriverの正しい終了とプロセス管理

テストが終了した後もドライバプロセスが残り続けると、次回のテスト開始時に競合を起こす可能性があります。pytestなどでテスト毎にブラウザを立ち上げる構成にしている場合、確実にdriver.quit()またはdriver.close()を呼び出しましょう。

pytestのfixtureでドライバを管理する例

import pytest
from selenium import webdriver

@pytest.fixture
def driver():
    driver = webdriver.Chrome()
    yield driver
    driver.quit()

def test_example(driver):
    driver.get("https://example.com")
    assert "Example" in driver.title

上記のようにfixtureを使うと、テスト実行後に必ずdriver.quit()が呼び出され、ChromeDriverのプロセスをクリーンに終了できます。これが積み重なると、大規模実行時の安定性に大きく寄与します。

4. テスト間のウェイトとスリープを適切に入れる

大量のテストを一斉に実行すると、同じタイミングで複数のChromeウィンドウを起動しようとして負荷が集中します。テストの開始前後に短いスリープ時間やウェイトを挟むことで、リソース取得の競合を緩和できます。

例: テスト終了前に待機を入れる

def teardown_function():
    # ここでブラウザを終了
    time.sleep(3)  # 次のテストに移行する前に少し待機

もちろん待機を入れすぎると総テスト時間が増えますが、実行台数を減らしたりジョブを分割するコストと比較して検討する価値はあります。

5. Jenkinsパイプラインのログ詳細化とエラートラッキング

それでも問題解決しない場合、まずはログを詳細化して根拠を得ることが大切です。

  • Jenkinsのコンソール出力を拡張する
  • pytestの-v(verbose)や--junitxmlなどのオプションで詳細ログを取得する
  • テスト実行ごとにChromeDriverのログ出力をファイルにリダイレクトする

たとえば以下のようにChromeDriverのログをファイル化すると、クラッシュの瞬間やどのタイミングで接続拒否が起きたかをより正確に把握できます。

webdriver.Chrome(service=Service(ChromeDriverManager().install(), 
                service_args=["--verbose", "--log-path=chromedriver.log"]))

JenkinsのログとChromeDriverのログを突き合わせることで、ポート競合かリソース不足かを切り分けられる場合があります。

より安定したテスト実行のための総合的なアプローチ

単一の施策では不十分な場合も多いため、複合的な対策を行うと効果的です。

  1. まずはChromeDriverとChromeのバージョンを最新かつ一致させる
  2. サーバーのCPU、メモリ、エフェメラルポート設定を見直し、必要に応じてスケールアップや分散実行を行う
  3. Jenkinsやpytestでの並列度を調整して、無理な同時実行を避ける
  4. テスト終了後のブラウザやChromeDriverプロセスの確実な終了(クリーンアップ)
  5. 十分なログ収集と検証を行い、問題を段階的に切り分けていく

まとめ

Selenium/Pythonによるテストを大量に同時実行するときに発生する「Failed to establish a new connection: [WinError 10061]」やChromeDriverのクラッシュは、主にリソース不足やポート競合、ブラウザとドライバのバージョン不整合などが原因となりがちです。まずはバージョン管理を徹底し、サーバーのスペックや並列度を見直すことで、格段にテストの安定度が高まります。加えて、テスト終了後のプロセス掃除やログ解析を行うことで、潜在的な問題を早期に発見して対処できるようになるはずです。

今後もテストが大規模化していくと、CI/CDのパイプライン内での工夫がますます重要になります。テスト実行の効率化と安定性を両立させるために、本記事で紹介したポイントをぜひ導入してみてください。自動化テストを活用して、開発や運用の品質向上に役立てられるよう、引き続き改善を継続していきましょう。

コメント

コメントする