QA業務でPythonが効くのは、UIテスト自動化・APIテスト・ログ解析・負荷テスト・CI連携の5領域です。どれも「手作業では回数をこなせない」ことが共通しています。
先に方針を書きます。5つを一度に導入しようとすると必ず失敗します。 効果が出るまでが早い順に、1つずつ足していくのが現実的です。この記事では、それぞれについてそのまま動くコードと、実務で詰まる点を書きます。
スポンサーリンク
QA業務のどこに効くのか
| 課題 | 使うもの | 導入の重さ |
|---|---|---|
| 回帰テストの負担が増え続ける | Playwright / Selenium | 重い |
| API変更で抜け漏れが出る | pytest + requests | 軽い |
| エラーログの目視確認に時間がかかる | pandas | 軽い |
| 負荷テストが実施できない | Locust | 中 |
| テストが手元でしか動かない | GitHub Actions / Jenkins | 中 |
最初に手を付けるなら API テストかログ解析です。 どちらも数十行で書けて、その日から効果が出ます。UIテストは効果も大きいですが、保守コストが高いので後回しで構いません。
1. UIテスト自動化:Playwright を優先する
これから新規に書くなら Playwright を推奨します。 待機処理が自動で入るため、Selenium で頻発する「通ったり落ちたりするテスト」がかなり減ります。
| 項目 | Selenium | Playwright |
|---|---|---|
| 待機処理 | 自分で書く(WebDriverWait) |
自動で待つ |
| ブラウザの用意 | Selenium Manager が自動化 | playwright install |
| 失敗時の調査 | 自分でスクショを実装 | トレース機能が標準 |
| 既存資産 | 情報量が多い | 比較的新しい |
Playwright の例
pip install pytest-playwright
playwright install chromium
from playwright.sync_api import Page, expect
def test_login(page: Page):
page.goto("https://example.com/login")
page.get_by_label("メールアドレス").fill("user@example.com")
page.get_by_label("パスワード").fill("password")
page.get_by_role("button", name="ログイン").click()
# 要素が現れるまで自動で待つ
expect(page.get_by_role("heading", name="マイページ")).to_be_visible()
get_by_role や get_by_label を使うのがポイントです。CSSクラス名で要素を探すと、デザイン変更のたびにテストが壊れます。ユーザーから見える情報(役割・ラベル)で探すと、実装が変わっても壊れにくくなります。
Selenium の例
既存資産がある場合は Selenium のままでも問題ありません。ただし待機処理と後片付けは必須です。
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
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com")
button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-testid='submit']"))
)
button.click()
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid='result']"))
)
finally:
driver.quit() # 失敗してもブラウザを閉じる
time.sleep() は使わないでください。 速い環境では無駄に待ち、遅い環境では足りません。Selenium・Appium・Jenkins の詳細はQAのテスト自動化にまとめています。
スポンサーリンク
2. APIテスト:pytest + requests
導入が最も軽く、効果が出るのも早い領域です。 数十行でリグレッション検出の仕組みができます。
pip install pytest requests
import pytest
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
@pytest.fixture(scope="session")
def session():
s = requests.Session()
s.headers.update({"Accept": "application/json"})
yield s
s.close()
def test_get_post(session):
res = session.get(f"{BASE_URL}/posts/1", timeout=10)
assert res.status_code == 200
data = res.json()
assert data["id"] == 1
assert isinstance(data["userId"], int) # 型も確認する
assert data["title"] # 空文字でないこと
@pytest.mark.parametrize("post_id,expected", [
(1, 200),
(100, 200),
(99999, 404), # 存在しないID
])
def test_status_codes(session, post_id, expected):
res = session.get(f"{BASE_URL}/posts/{post_id}", timeout=10)
assert res.status_code == expected
押さえるべき点が3つあります。
timeoutを必ず指定する:付けないと応答が返らないときテストが永久に止まります- 存在だけでなく型も確認する:
userIdが数値のはずが文字列で返る、という不具合はassert "userId" in dataでは検出できません parametrizeで境界値をまとめる:同値分割と境界値分析で洗い出した値をそのまま並べられます
Postman のコレクションを CI で動かす場合
Postman でシナリオを作ってある場合、pytest に書き直す必要はありません。 Newman を使えばそのまま実行できます。
npm install -g newman
newman run collection.json \
-e environment.json \
--reporters cli,junit \
--reporter-junit-export results.xml
Postman 側のスクリプトの書き方はPostmanのPre-requestとPost-responseにまとめました。
「Postman で書くか pytest で書くか」は、チームの状況で決めてください。 非エンジニアもテストを書くなら Postman、コードレビューに載せたいなら pytest が向きます。
3. ログ解析:pandas で目視をやめる
エラーログを目で追う作業は、真っ先に自動化すべきです。
import pandas as pd
df = pd.read_csv("logs.csv", parse_dates=["timestamp"], encoding="utf-8-sig")
# エラーだけに絞る
errors = df[df["level"] == "ERROR"]
# 多い順に集計する
counts = errors["error_message"].value_counts()
print(counts.head(10))
# 時間帯別の発生数を見る
by_hour = errors.set_index("timestamp").resample("1h").size()
print(by_hour[by_hour > 0])
resample() で時間帯別に集計するのが有効です。 「特定の時刻に集中している」と分かれば、バッチ処理やデプロイとの関連を疑えます。総数だけ見ていても気づけません。
可視化するときは日本語フォントに注意
設定しないと、日本語のラベルが四角(□)になります。
pip install matplotlib matplotlib-fontja
import matplotlib.pyplot as plt
import matplotlib_fontja # インポートするだけで日本語が通る
fig, ax = plt.subplots(figsize=(10, 5))
counts.head(10).sort_values().plot(kind="barh", ax=ax)
ax.set_title("エラーメッセージ 上位10件")
ax.set_xlabel("発生件数")
plt.tight_layout()
fig.savefig("errors.png", dpi=150, bbox_inches="tight")
2つコツがあります。
- 横棒グラフ(
barh)にする:エラーメッセージは長いので、縦棒だとラベルが読めません sort_values()してから描く:並び順がバラバラだと比較になりません
グラフ描画の詳細はPandasとMatplotlibを活用したデータ可視化にまとめています。
異常検知の簡単な方法
統計的なしきい値を使うと、「いつもと違う」を機械的に拾えます。
# 直近の平均と標準偏差から外れ値を出す
mean = by_hour.mean()
std = by_hour.std()
threshold = mean + 3 * std
spikes = by_hour[by_hour > threshold]
if not spikes.empty:
print("異常なエラー増加を検出しました")
print(spikes)
固定のしきい値(「100件を超えたら通知」など)は運用が続きません。 アクセス数が増えればエラー数も増えるためです。平均からの乖離で見る方が保守が楽です。
スポンサーリンク
4. 負荷テスト:Locust
Python でシナリオを書けるのが利点です。JMeter の GUI より、ユーザーの行動を再現しやすくなります。
pip install locust
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
# 各アクションの間に1〜3秒の間隔を空ける
wait_time = between(1, 3)
def on_start(self):
"""各仮想ユーザーの開始時に1回だけ実行される"""
res = self.client.post("/api/login", json={
"email": "user@example.com",
"password": "password",
})
self.token = res.json().get("token")
@task(3) # 重み3:他より3倍多く実行される
def view_top(self):
self.client.get("/", name="トップページ")
@task(1)
def search(self):
self.client.get("/search?q=test", name="検索")
@task(1)
def view_detail(self):
# URLが可変でも name を揃えると集計が1行にまとまる
self.client.get("/items/123", name="/items/[id]")
重要なのは3点です。
wait_timeを必ず設定する:省略すると仮想ユーザーが休みなくリクエストを送り、現実にはあり得ない負荷になりますnameで集計をまとめる:/items/123と/items/456が別項目として集計されると、結果が読めません@taskの重みで実際の利用比率に寄せる:全ページを均等に叩いても実態を反映しません
CI から実行する場合は、GUI を使わずヘッドレスで動かします。
locust -f locustfile.py \
--headless \
--users 100 \
--spawn-rate 10 \
--run-time 3m \
--host https://staging.example.com \
--html report.html
--spawn-rate(1秒あたりの増加人数)をユーザー数と同じにしないでください。 全員が同時に接続する状況は現実には起きず、測定結果が実態とかけ離れます。
結果の読み方
平均レスポンスタイムだけを見てはいけません。
| 指標 | 見方 |
|---|---|
| 平均 | 参考程度。外れ値に引っ張られる |
| 95%タイル | 実質的な体感速度。ここを基準にする |
| 失敗率 | 0%でなければ原因を特定する |
| RPS | 頭打ちになる地点がシステムの限界 |
平均250msでも、95%タイルが3秒なら20人に1人が3秒待っています。
また、負荷試験は必ず許可された環境で実施してください。 本番環境や、自分に権限のないサービスに負荷をかけてはいけません。
5. CI との統合
手元で実行しているうちは、自動テストの価値は半分以下です。 「変更のたびに自動で走り、落ちたら気づける」状態にして初めて効果が出ます。
GitHub Actions の例
name: API Test
on:
pull_request:
schedule:
- cron: '0 0 * * *' # 毎日実行する
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r requirements.txt
- run: pytest tests/ --junitxml=results.xml
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
- name: テスト結果をアップロード
if: always() # 失敗しても結果を残す
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
results.xml
screenshots/
2点が重要です。
if: always()を付ける:これが無いと、テストが失敗したときに結果ファイルが残らず、原因を調べられません- 認証情報は
secretsから渡す:コードに直書きしない
UIテストでは失敗時のスクリーンショットを残す
# conftest.py
import pytest
from pathlib import Path
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
page = item.funcargs.get("page")
if page:
Path("screenshots").mkdir(exist_ok=True)
page.screenshot(path=f"screenshots/{item.name}.png", full_page=True)
これが無いと、CIで落ちたUIテストの原因はほぼ分かりません。 ログだけでは画面の状態が再現できないためです。
導入する順番
一度に全部やろうとすると、どれも中途半端になります。効果が出るまでが早い順に進めてください。
- 1. ログ解析:数十行で書けて、その日から効く。誰の合意も要らない
- 2. APIテスト:UIより壊れにくく、保守が軽い。主要エンドポイントから
- 3. CI連携:ここまでを自動実行に載せる
- 4. UIテスト:主要導線を3〜5本だけ。網羅は狙わない
- 5. 負荷テスト:必要になったタイミングで
最も避けたいのは「落ちても誰も見ないテスト」です。 そうなった時点で、あるだけ負債になります。本数を増やすより、落ちたら必ず対応される状態を先に作ってください。
まとめ
- 新規のUIテストは Playwright を優先。
get_by_roleで要素を探すと壊れにくい - Selenium を使うなら
WebDriverWaitとtry/finallyが必須。time.sleep()は使わない - APIテストは
timeoutを必ず指定し、存在だけでなく型も確認する - Postman の資産があるなら Newman でそのまま CI に載せる。書き直さなくてよい
- ログは
resample()で時間帯別に見る。グラフはmatplotlib-fontjaで日本語対応 - 異常検知は固定しきい値ではなく平均からの乖離で
- Locust は
wait_timeとnameを必ず設定。95%タイルで評価する - CI では
if: always()で結果を残し、失敗時のスクリーンショットを保存する - 導入順はログ解析 → APIテスト → CI連携 → UIテスト → 負荷テスト
ツール選定より前に、「何を守りたいか」を決めるのが先です。その判断軸はE2Eで「守るべきもの」はこう決めるにまとめています。