The standard starting set for test automation is Selenium (or Playwright) for the web, Appium for mobile, Jenkins to run it all, and JMeter for load testing.

This article was published in 2024 and revised in September 2026. The Appium code shown at the time uses a form that Appium 2 removed and no longer runs. The Selenium sections also carried some outdated assumptions, now updated.

  • Appium 2 removed the /wd/hub base path. The default is now /
  • Selenium 4.10 removed the desired_capabilities argument. Pass an Options object via options=
  • Selenium 4.6 and later include Selenium Manager, so you no longer download ChromeDriver by hand

Decide what to automate first

Automating your manual test suite as-is will fail.

A check worth doing once is not the same as a check worth repeating. Automation suits:

Good fit Poor fit
Primary flows verified every release One-off acceptance checks
Features where breakage is costly (payment, login) Fine visual adjustments
Volumes impractical by hand (many combinations) Screens still under active design
Load testing, which people cannot do at all Exploratory testing

Writing many E2E tests against a moving specification burns your budget on maintaining tests. Decide what you are protecting before you start—covered in what to protect with E2E tests.

 

Selenium: automating the browser

Installation is now just pip

pip install selenium

 

From Selenium 4.6, driver setup is automatic. Selenium Manager fetches a driver matching the installed browser.

The old failure mode—”Chrome updated, ChromeDriver no longer matches, every test fails”—is gone. Instructions in older articles about installing webdriver-manager are no longer needed.

The basic shape

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()   # close the browser even on failure

 

Waiting is where tests are won or lost

The most common cause of automation failure is how you wait. Get it wrong and you produce flaky tests that pass and fail unpredictably.

Approach Verdict Why
time.sleep(5) Do not use Wasteful when fast, insufficient when slow
implicitly_wait(5) Limited Only waits for elements to exist
WebDriverWait + expected_conditions Recommended Waits on a stated condition

implicitly_wait() is a setting, not a wait instruction. Calling it does not pause anything, so it cannot express “wait until the results appear”. Use WebDriverWait.

Also, do not mix implicitly_wait() with WebDriverWait—the combined timeouts become unpredictable.

Locators

# avoid string locators
driver.find_element("name", "q")

# use By
driver.find_element(By.NAME, "q")
driver.find_element(By.CSS_SELECTOR, "[data-testid='submit']")

 

A dedicated test attribute such as data-testid is the most durable locator. CSS class names break with every redesign, and long absolute XPath expressions break when a single element is inserted.

 

Appium: automating mobile apps

Appium sits on the same WebDriver protocol as Selenium, so the code reads similarly—but Appium 2 changed how you connect.

Appium 2 setup

npm install -g appium
appium driver install uiautomator2   # install the Android driver separately
appium                              # starts on http://localhost:4723

 

Appium 2 separated drivers from the core. Install the driver for each platform you target—uiautomator2 for Android, xcuitest for iOS.

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",
}

# wrap them in an Options object
options = UiAutomator2Options().load_capabilities(capabilities)

# the base path is / , not /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 (old) Appium 2 (current)
Server URL http://localhost:4723/wd/hub http://localhost:4723
Capabilities Second positional argument An Options object via options=
Vendor capabilities No prefix appium: prefix required
Drivers Bundled Installed with appium driver install

Forgetting the appium: prefix is another common stumble. Every capability not standardised in the W3C specification needs it—platformName is the exception, being standard.

Prefer accessibility ids

AppiumBy.ACCESSIBILITY_ID maps to content-desc on Android and accessibilityIdentifier on iOS. It is the easiest way to share test code across both platforms, so it is worth asking the development team to set them.

 

Jenkins: making it run continuously

Automation scripts have limited value while you run them by hand. The value appears once they run on every change and someone notices when they fail.

The usual flow:

  • A developer pushes to GitHub
  • Jenkins detects the change
  • Selenium and Appium tests run
  • Results go to Slack or email

Keeping a Jenkinsfile in the repository beats configuring shell steps in the UI—the configuration becomes reviewable code.

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'
      archiveArtifacts artifacts: 'screenshots/**', allowEmptyArchive: true
    }
  }
}

 

Capture screenshots on failure

Without this you cannot diagnose a CI failure. Logs alone do not reconstruct what was on screen.

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

 

Do not paper over flaky tests with retries. Making them pass on a second attempt also hides genuine defects. Find the cause and fix the waiting.

 

JMeter: load testing

Questions like “what happens when 1,000 people hit this API at once” cannot be answered manually.

Three parameters define a run.

Setting Meaning
Thread count Concurrent virtual users
Ramp-up period Seconds taken to reach that count
Loop count Repetitions per user

Do not set ramp-up to zero. Everyone connecting simultaneously creates a spike that does not occur in reality, and the numbers stop meaning anything.

Reading the results

Never judge by the mean alone.

Metric How to read it
Mean response time Indicative only; outliers distort it
90th / 95th percentile What users actually experience. Judge on this
Error rate Anything above zero needs investigating
Throughput Find where it plateaus

A 250ms mean with a 3-second 95th percentile means one user in twenty waits three seconds. Averages conceal problems—always read the percentiles.

In CI, run it headless rather than through the GUI.

jmeter -n -t test-plan.jmx -l result.jtl -e -o report/

 

The JMeter GUI is not for generating load. Use it to build the plan, then always run with -n. Loading through the GUI makes JMeter itself the bottleneck and skews the measurements.

For load tests written in Python, Locust is an alternative—see 5 ways to make QA work faster with Python.

 

The order to introduce these

Trying to stand everything up at once fails. Add one piece at a time, in order of payoff.

  • 1. Write three to five E2E tests for primary flows: login, purchase, contact. Do not aim for coverage
  • 2. Run them in CI: local-only runs become ceremonial
  • 3. Keep screenshots and reports on failure: without diagnosis, failures get ignored
  • 4. Add more once they are stable: flaky tests destroy trust in the whole suite

The worst outcome is a suite whose failures nobody looks at. At that point it is pure liability. Establishing that failures always get acted on matters more than the number of tests.

 

Summary

  • Selenium 4.6+ needs no manual driver setup (Selenium Manager)
  • Wait with WebDriverWait—not time.sleep(), not implicitly_wait()
  • Locate elements by a dedicated attribute such as data-testid
  • Appium 2 changed the base path from /wd/hub to /
  • Wrap capabilities in UiAutomator2Options and pass via options=. Do not forget the appium: prefix
  • Keep the Jenkinsfile in the repo and always save screenshots on failure
  • Run JMeter headless (-n) and judge on the 95th percentile, not the mean
  • Establish “failures always get acted on” before growing the suite

Tooling information dates quickly here. Checking the version in the official documentation before you start saves a surprising amount of time.

ABOUT ME
りん
On this blog, I mainly share information about web development and programming, along with my daily thoughts and what I’ve learned. I aim to create a blog that lets readers enjoy both technology and everyday life, so I also include topics about daily experiences, books, and gourmet. I’d be delighted if you could drop by casually and find something useful or enjoyable here.