Browse by section

QA 日本語

5 Ways to Make QA Work Faster with Python

Python helps QA work in five areas: UI test automation, API testing, log analysis, load testing, and CI integration. What they share is that none of them can be done repeatedly by hand.

One piece of advice up front: trying to introduce all five at once will fail. Add them one at a time, in order of how quickly they pay off. This article gives working code for each, plus the parts that trip people up.

Sponsored

Where Python actually helps

Problem Tool Effort
Regression testing keeps growing Playwright / Selenium High
API changes slip through pytest + requests Low
Reading error logs by eye takes hours pandas Low
Load testing is not happening Locust Medium
Tests only run on someone’s laptop GitHub Actions / Jenkins Medium

Start with API testing or log analysis. Both take a few dozen lines and pay off the same day. UI testing has a large payoff too, but a high maintenance cost—it can wait.

1. UI automation: prefer Playwright

For new work, Playwright is the better choice. Its automatic waiting eliminates most of the flakiness that plagues Selenium suites.

Selenium Playwright
Waiting You write it (WebDriverWait) Automatic
Browser setup Selenium Manager handles it playwright install
Failure diagnosis Implement screenshots yourself Tracing built in
Existing material Abundant Newer

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("Email").fill("user@example.com")
    page.get_by_label("Password").fill("password")
    page.get_by_role("button", name="Log in").click()

    # waits automatically for the element to appear
    expect(page.get_by_role("heading", name="My page")).to_be_visible()

Use get_by_role and get_by_label. Finding elements by CSS class means tests break on every redesign; finding them by what users perceive—role and label—survives implementation changes.

Selenium

Existing suites are fine to keep. But waiting and cleanup are non-negotiable.

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()

Never use time.sleep()—too slow on fast machines, too short on slow ones. Details in test automation with Selenium, Appium and Jenkins.

Sponsored

2. API testing: pytest and requests

The lightest to adopt and the fastest to pay off. A few dozen lines gives you regression detection.

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)   # check the type as well
    assert data["title"]                     # not an empty string

@pytest.mark.parametrize("post_id,expected", [
    (1, 200),
    (100, 200),
    (99999, 404),   # nonexistent 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
  • Always pass timeout, or a hanging server hangs your test run forever
  • Assert types, not just presence. A userId arriving as a string slips past assert "userId" in data
  • Use parametrize for boundaries—drop in the values from equivalence partitioning and boundary value analysis

Running Postman collections in CI

If you already have Postman scenarios, you do not need to rewrite them. Newman runs them directly.

npm install -g newman

newman run collection.json \
  -e environment.json \
  --reporters cli,junit \
  --reporter-junit-export results.xml

Postman scripting is covered in Pre-request and Post-response in Postman.

Postman or pytest depends on your team. Postman if non-engineers write tests; pytest if you want them in code review.

3. Log analysis: stop reading by eye

This is the first thing to automate.

import pandas as pd

df = pd.read_csv("logs.csv", parse_dates=["timestamp"], encoding="utf-8-sig")

# errors only
errors = df[df["level"] == "ERROR"]

# most frequent first
counts = errors["error_message"].value_counts()
print(counts.head(10))

# occurrences by hour
by_hour = errors.set_index("timestamp").resample("1h").size()
print(by_hour[by_hour > 0])

Bucketing by time with resample() is the useful part. “They cluster at a particular hour” points you at a batch job or a deployment. Totals alone never reveal that.

Charts with non-Latin labels

Without configuration, Japanese labels render as boxes.

pip install matplotlib matplotlib-fontja
import matplotlib.pyplot as plt
import matplotlib_fontja   # importing is enough

fig, ax = plt.subplots(figsize=(10, 5))
counts.head(10).sort_values().plot(kind="barh", ax=ax)

ax.set_title("Top 10 error messages")
ax.set_xlabel("Occurrences")

plt.tight_layout()
fig.savefig("errors.png", dpi=150, bbox_inches="tight")
  • Use horizontal bars (barh)—error messages are long and unreadable on a vertical axis
  • Call sort_values() first, or the chart is not a comparison

More on plotting in data visualisation with Pandas and Matplotlib.

Simple anomaly detection

# flag hours that deviate from the norm
mean = by_hour.mean()
std = by_hour.std()
threshold = mean + 3 * std

spikes = by_hour[by_hour > threshold]

if not spikes.empty:
    print("Unusual error volume detected")
    print(spikes)

Fixed thresholds (“alert above 100”) do not survive contact with reality—error counts rise with traffic. Deviation from the mean is easier to maintain.

Sponsored

4. Load testing: Locust

Writing scenarios in Python makes it easier to reproduce real user behaviour than a GUI tool.

pip install locust
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    # wait one to three seconds between actions
    wait_time = between(1, 3)

    def on_start(self):
        """runs once per virtual user"""
        res = self.client.post("/api/login", json={
            "email": "user@example.com",
            "password": "password",
        })
        self.token = res.json().get("token")

    @task(3)   # weight 3: runs three times as often
    def view_top(self):
        self.client.get("/", name="top page")

    @task(1)
    def search(self):
        self.client.get("/search?q=test", name="search")

    @task(1)
    def view_detail(self):
        # a shared name keeps variable URLs in one row of the report
        self.client.get("/items/123", name="/items/[id]")
  • Always set wait_time—without it virtual users hammer without pause, producing load nobody generates in reality
  • Use name to group URLs. /items/123 and /items/456 reported separately make results unreadable
  • Weight the tasks to match real usage. Hitting every page equally does not reflect anything

Run it headless in CI.

locust -f locustfile.py \
  --headless \
  --users 100 \
  --spawn-rate 10 \
  --run-time 3m \
  --host https://staging.example.com \
  --html report.html

Do not set --spawn-rate equal to the user count. Everyone arriving at once is not a situation that occurs.

Reading results

Metric How to read it
Mean Indicative only; outliers distort it
95th percentile What users experience. Judge on this
Failure rate Anything above zero needs a cause
RPS Where it plateaus is your ceiling

Only run load tests against environments you are authorised to test. Never against production or a service you do not control.

5. CI integration

Tests that live on a laptop deliver less than half their value.

name: API Test

on:
  pull_request:
  schedule:
    - cron: '0 0 * * *'   # nightly

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: Upload results
        if: always()      # keep artefacts even on failure
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: |
            results.xml
            screenshots/
  • if: always() matters. Without it, a failing run leaves no artefacts to investigate
  • Pass credentials from secrets, never in code

Screenshots on UI test failure

# 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)

Without this, a failed UI test in CI is essentially undiagnosable.

The order to adopt them

Work through them by payoff speed.

  • 1. Log analysis: dozens of lines, effective immediately, needs nobody’s approval
  • 2. API testing: less brittle than UI, cheap to maintain. Start with core endpoints
  • 3. CI integration: get the above running automatically
  • 4. UI testing: three to five primary flows. Do not chase coverage
  • 5. Load testing: when you actually need it

The worst outcome is a suite whose failures nobody looks at. From that moment it is a liability. Establishing that failures always get acted on matters more than the count.

Summary

  • For new UI tests prefer Playwright, locating elements with get_by_role
  • With Selenium, WebDriverWait and try/finally are mandatory. Never time.sleep()
  • API tests need timeout, and should assert types as well as presence
  • Existing Postman collections can run in CI via Newman—no rewrite needed
  • Use resample() for logs, and matplotlib-fontja for non-Latin chart labels
  • Detect anomalies by deviation from the mean, not a fixed threshold
  • Locust needs wait_time and name. Judge on the 95th percentile
  • In CI use if: always() and save screenshots on failure
  • Adopt in this order: logs → API → CI → UI → load

Before choosing tools, decide what you are trying to protect. That framing is covered in what to protect with E2E tests.