Selenium・Appium・Jenkinsを使ったQAのテスト自動化
テスト自動化を始めるなら、Web は Selenium(または Playwright)、モバイルは Appium、実行の仕組み化は Jenkins、負荷テストは JMeterという組み合わせが基本形です。
この記事は2024年の公開後、2026年9月に見直しました。 公開当時の Appium のコードは、Appium 2 で廃止された書き方だったため動きません。Selenium 側も、いくつか古い前提が残っていたので現行の書き方に差し替えています。
- Appium 2 で接続先の
/wd/hubは廃止されました。既定のベースパスは/です - Selenium 4.10 で
desired_capabilities引数が削除されました。options=に Options オブジェクトを渡します - Selenium 4.6 以降は Selenium Manager が内蔵され、ChromeDriver などを手動でダウンロードする必要がなくなりました
何を自動化すべきか
先に方針を書きます。「手動テストをそのまま自動化する」と失敗します。
手動で1回やれば済む確認と、繰り返し実行する価値がある確認は別物です。自動化に向くのは次のようなものです。
| 向いている | 向いていない |
|---|---|
| リリースのたびに必ず確認する主要導線 | 1回しか実行しない受入確認 |
| 壊れると事業影響が大きい機能(決済、ログイン) | 見た目の細かい調整 |
| 手作業では現実的でない量(多数の組み合わせ) | 仕様が固まっていない開発中の画面 |
| 負荷試験のような人手では不可能な検証 | 探索的テスト |
仕様が動いている画面のE2Eを大量に書くと、テストの修正だけで工数が溶けます。 何を守るかを決めてから書き始めてください。この判断軸はE2Eで「守るべきもの」はこう決めるにまとめています。
Selenium:Webブラウザの操作を自動化する
導入は pip だけで済むようになった
pip install selenium
Selenium 4.6 以降は、ドライバの準備が不要です。 Selenium Manager がブラウザのバージョンに合ったドライバを自動で用意します。
以前は「Chrome を更新したら ChromeDriver のバージョンが合わなくなってテストが全部落ちる」という定番のトラブルがありましたが、この作業はもう要りません。古い記事にある webdriver-manager の導入手順も、今は不要です。
基本のコード
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
# options.add_argument("--headless=new") # 画面を出さずに実行する
driver = webdriver.Chrome(options=options)
try:
driver.get("https://www.selenium.dev/")
# 要素を明示的に待つ
wait = WebDriverWait(driver, 10)
search_box = wait.until(
EC.element_to_be_clickable((By.NAME, "q"))
)
search_box.send_keys("webdriver")
search_box.send_keys(Keys.RETURN)
# 結果が出るまで待つ
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".search-result")))
finally:
driver.quit() # 失敗しても必ずブラウザを閉じる
待機の書き方が最重要
テスト自動化で最も多い失敗が「待ち方」です。 ここを間違えると、通ったり落ちたりする不安定なテスト(flaky test)になります。
| 書き方 | 評価 | 理由 |
|---|---|---|
time.sleep(5) |
使わない | 速い環境では無駄、遅い環境では足りない |
implicitly_wait(5) |
限定的 | 「要素が見つかるまで」しか待てない |
WebDriverWait + expected_conditions |
推奨 | 条件を指定して待てる |
implicitly_wait() は「待機を実行する命令」ではなく「設定」です。 呼んだ時点で止まるわけではありません。「検索結果が表示されるまで待つ」という用途には使えないので、WebDriverWait を使ってください。
また、implicitly_wait() と WebDriverWait の併用は避けてください。 待ち時間が予測できない形で伸びることがあります。
要素の探し方
# 文字列指定は避ける
driver.find_element("name", "q")
# By を使う
driver.find_element(By.NAME, "q")
driver.find_element(By.CSS_SELECTOR, "[data-testid='submit']")
セレクターには data-testid のようなテスト専用属性を使うのが最も壊れにくいです。CSSクラス名を使うと、デザイン変更のたびにテストが落ちます。XPath の長い絶対パスは最悪で、要素が1つ増えるだけで壊れます。
Appium:モバイルアプリのテストを自動化する
Appium は Selenium と同じ WebDriver プロトコルの上に作られているため、書き味が似ています。ただしAppium 2 で接続方法が変わりました。
Appium 2 での書き方
npm install -g appium
appium driver install uiautomator2 # Android 用ドライバを個別に入れる
appium # サーバー起動(http://localhost:4723)
Appium 2 ではドライバが本体から分離されました。 使うプラットフォームのドライバを個別にインストールする必要があります(Android は uiautomator2、iOS は xcuitest)。
pip install Appium-Python-Client
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
capabilities = {
"platformName": "Android",
"appium:automationName": "UiAutomator2",
"appium:deviceName": "emulator-5554",
"appium:app": "/path/to/app.apk",
}
# Options オブジェクトに詰めて渡す
options = UiAutomator2Options().load_capabilities(capabilities)
# ベースパスは /wd/hub ではなく /
driver = webdriver.Remote("http://localhost:4723", options=options)
try:
wait = WebDriverWait(driver, 20)
login_button = wait.until(EC.element_to_be_clickable(
(AppiumBy.ID, "com.example.app:id/login_button")
))
login_button.click()
wait.until(EC.presence_of_element_located(
(AppiumBy.ACCESSIBILITY_ID, "home-screen")
))
finally:
driver.quit()
公開当時のコードとの違いを整理します。
| 項目 | Appium 1(旧) | Appium 2(現行) |
|---|---|---|
| 接続先 | http://localhost:4723/wd/hub |
http://localhost:4723 |
| capabilities の渡し方 | 第2引数にそのまま | options= に Options オブジェクト |
| ベンダー固有のcap | 接頭辞なし | appium: 接頭辞が必要 |
| ドライバ | 本体に同梱 | appium driver install で個別に |
appium: 接頭辞の付け忘れも定番のつまずきです。 W3C 仕様で標準化されていないキーには、すべてこの接頭辞が必要です(platformName だけは標準なので不要)。
要素の指定は accessibility id を優先する
AppiumBy.ACCESSIBILITY_ID は、Android の content-desc と iOS の accessibilityIdentifier の両方に対応します。iOS と Android でテストコードを共通化しやすくなるので、開発チームに設定を依頼する価値があります。
Jenkins:テストを回し続ける仕組みにする
自動化スクリプトは、手元で実行しているうちは効果が限定的です。 「変更のたびに自動で走り、落ちたら気づける」状態にして初めて価値が出ます。
典型的な流れは次の通りです。
- 開発者が GitHub にプッシュする
- Jenkins が変更を検知する
- Selenium / Appium のテストを実行する
- 結果を Slack やメールに通知する
シェルスクリプトを直に書くよりも、Jenkinsfile をリポジトリに置く方が管理しやすいです。設定がコードとしてレビューできます。
pipeline {
agent any
stages {
stage('Setup') {
steps {
sh 'python -m venv .venv'
sh '.venv/bin/pip install -r requirements.txt'
}
}
stage('Test') {
steps {
// 失敗しても後続のレポート収集まで進めたい場合
sh '.venv/bin/pytest tests/ --junitxml=results.xml'
}
}
}
post {
always {
junit 'results.xml' // 結果をJenkinsに取り込む
archiveArtifacts artifacts: 'screenshots/**', allowEmptyArchive: true
}
}
}
失敗時にスクリーンショットを残す
これが無いと、CIで落ちたテストの原因が分かりません。 ログだけでは「何が表示されていたか」が再現できないためです。
import pytest
from pathlib import Path
@pytest.fixture
def driver():
d = webdriver.Chrome(options=options)
yield d
d.quit()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
drv = item.funcargs.get("driver")
if drv:
Path("screenshots").mkdir(exist_ok=True)
drv.save_screenshot(f"screenshots/{item.name}.png")
不安定なテストを retry で誤魔化さないでください。 再実行で通ることにすると、本物の不具合まで見逃します。落ちたら原因を特定して待機処理を直すのが正しい対応です。
JMeter:負荷テストを自動化する
「APIに1000人が同時アクセスしたらどうなるか」といった検証は、人手では不可能です。
設定するのは3つのパラメータです。
| 設定 | 意味 |
|---|---|
| スレッド数 | 同時に動く仮想ユーザー数 |
| Ramp-up 期間 | その人数まで増やすのにかける秒数 |
| ループ回数 | 各ユーザーが何回繰り返すか |
Ramp-up をゼロにしないでください。 全員が同時に接続すると現実には起こらない瞬間的なピークを作ってしまい、測定結果が実態とかけ離れます。
見るべき指標
結果を読むときに平均値だけを見てはいけません。
| 指標 | 見方 |
|---|---|
| 平均レスポンスタイム | 参考程度。外れ値に引っ張られる |
| 90%タイル / 95%タイル | 実質的な体感速度。ここを基準にする |
| エラーレート | 0%でないなら原因を特定する |
| スループット | 単位時間あたりの処理数。頭打ちの地点を探す |
「平均250ms」でも、95%タイルが3秒なら20人に1人が3秒待っています。 平均値は問題を隠すので、必ずパーセンタイルを確認してください。
CI に組み込む場合は、GUI ではなく非GUIモードで実行します。
jmeter -n -t test-plan.jmx -l result.jtl -e -o report/
JMeter の GUI は負荷をかけるためのものではありません。 シナリオ作成には使いますが、実行時は必ず -n(非GUI)で走らせてください。GUIのまま負荷をかけると、JMeter 自身がボトルネックになって測定値が歪みます。
Python で負荷テストを書きたい場合は Locust という選択肢もあります。Python活用でQA業務を効率化する5つの方法で扱っています。
導入するときの順番
いきなり全部揃えようとすると挫折します。効果が出る順に、1つずつ足していくのが現実的です。
- 1. 主要導線のE2Eを3〜5本だけ書く:ログイン、購入、問い合わせなど。網羅は狙わない
- 2. CIで自動実行する:手元実行のままでは形骸化します
- 3. 失敗時のスクリーンショットとレポートを残す:原因追跡ができないと放置されます
- 4. 安定してから本数を増やす:不安定なテストが混ざると全体が信用されなくなります
最も避けたいのは「落ちても誰も見ないテスト」です。 そうなると、あるだけで負債になります。本数より、落ちたときに必ず対応される状態を作る方が先です。
まとめ
- Selenium 4.6 以降はドライバの手動準備が不要(Selenium Manager 内蔵)
- 待機は
time.sleep()でもimplicitly_wait()でもなくWebDriverWait - セレクターは
data-testidのようなテスト専用属性を使う - Appium 2 では接続先が
/wd/hubから/に変わった - capabilities は
UiAutomator2Optionsなどに詰めてoptions=で渡す。appium:接頭辞を忘れない - Jenkins では
Jenkinsfileをリポジトリ管理し、失敗時のスクリーンショットを必ず残す - JMeter は非GUIモード(
-n)で実行し、平均ではなく95%タイルを見る - 本数より「落ちたら必ず対応される状態」を先に作る
ツールの情報は古くなりやすい分野です。公式ドキュメントのバージョンを確認してから手を動かすだけで、無駄な調査時間をかなり減らせます。