Back to All Articles
Automation

Automation Testing Introduction

Honnesh Muppala May 5, 2026 14 min read

What is Automation Testing?

Automation Testing is the use of software tools and scripts to execute test cases automatically, compare actual outcomes with expected results, and report findings — without human intervention at execution time.

Rather than a tester manually clicking through an application, an automation script performs the same steps programmatically, often in seconds. This makes it possible to run thousands of tests in the time it would take a human to run dozens.

Automation is not a replacement for manual testing. It excels at repetitive, high-frequency, and data-driven scenarios. Manual testing excels at exploratory, UX, and context-sensitive scenarios. Strong QA engineers master both.

When to Automate

Not every test is a good candidate for automation. Here is a practical guide:

Automate Keep Manual
Regression test suites run every sprint Exploratory testing
Smoke and sanity checks on every build Usability / UX evaluation
Data-driven tests (many input combinations) One-off tests unlikely to be repeated
Performance and load testing Ad hoc / investigative testing
Cross-browser / cross-device validation Tests where requirements change frequently
CI/CD pipeline integration tests Tests requiring human judgement

Popular Automation Tools

The right tool depends on what you are testing — web, mobile, API, or desktop. Here are the three tools I have used most extensively in my career.

Selenium

Selenium is the industry standard for web browser automation. It supports multiple programming languages (Python, Java, JavaScript, C#) and all major browsers (Chrome, Firefox, Safari, Edge).

Best for

Key components

Appium

Appium is an open-source tool for automating native, hybrid, and mobile web apps on Android, iOS, and Fire OS. It uses the WebDriver protocol, so the API feels familiar if you already know Selenium.

Why Appium stands out

Best for

From Experience: At Virtusa, I built and maintained Appium test suites for Android and Fire OS applications. The biggest challenge was flakiness — mobile tests are sensitive to timing, network conditions, and device state. Explicit waits, proper element locator strategies (avoiding XPath where possible), and a clean setup/teardown lifecycle made our suite reliable enough to run in our Maven CI build.

Robot Framework

Robot Framework is a keyword-driven test automation framework that uses plain English syntax. It is particularly popular in teams where both technical and non-technical stakeholders contribute to test design.

Best for

During my MSc in DevOps at ATU Letterkenny, Robot Framework was part of our CI/CD pipeline module — integrated with Jenkins for automated test reporting on every build.

Sample Appium Script (Python)

Below is a basic Appium test for an Android app — the kind of script I wrote regularly at Virtusa. It launches the app, finds an element, and verifies text.

from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
import unittest

class AndroidLoginTest(unittest.TestCase):

    def setUp(self):
        desired_caps = {
            "platformName": "Android",
            "deviceName": "emulator-5554",
            "appPackage": "com.example.myapp",
            "appActivity": ".MainActivity",
            "automationName": "UIAutomator2",
            "noReset": True
        }
        self.driver = webdriver.Remote(
            "http://localhost:4723/wd/hub",
            desired_caps
        )
        self.driver.implicitly_wait(10)

    def test_login_with_valid_credentials(self):
        # Enter username
        username_field = self.driver.find_element(
            AppiumBy.ID, "com.example.myapp:id/username"
        )
        username_field.clear()
        username_field.send_keys("testuser@example.com")

        # Enter password
        password_field = self.driver.find_element(
            AppiumBy.ID, "com.example.myapp:id/password"
        )
        password_field.clear()
        password_field.send_keys("SecurePass123")

        # Tap login button
        login_button = self.driver.find_element(
            AppiumBy.ID, "com.example.myapp:id/loginBtn"
        )
        login_button.click()

        # Verify dashboard is displayed
        dashboard_title = self.driver.find_element(
            AppiumBy.ID, "com.example.myapp:id/dashboardTitle"
        )
        self.assertEqual(dashboard_title.text, "Welcome, testuser!")

    def tearDown(self):
        self.driver.quit()

if __name__ == "__main__":
    unittest.main()

Key points from this script

Sample Selenium Script (Python)

A simple Selenium test for web login using Chrome:

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
import unittest

class WebLoginTest(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.wait = WebDriverWait(self.driver, 10)

    def test_login_success(self):
        self.driver.get("https://example.com/login")

        # Enter credentials
        self.driver.find_element(By.ID, "email").send_keys("user@example.com")
        self.driver.find_element(By.ID, "password").send_keys("Password123")
        self.driver.find_element(By.ID, "loginBtn").click()

        # Wait for dashboard to load and verify
        dashboard = self.wait.until(
            EC.presence_of_element_located((By.ID, "dashboard-header"))
        )
        self.assertIn("Dashboard", dashboard.text)

    def test_login_invalid_password(self):
        self.driver.get("https://example.com/login")

        self.driver.find_element(By.ID, "email").send_keys("user@example.com")
        self.driver.find_element(By.ID, "password").send_keys("WrongPassword")
        self.driver.find_element(By.ID, "loginBtn").click()

        # Verify error message appears
        error = self.wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, "error-message"))
        )
        self.assertEqual(error.text, "Invalid email or password.")

    def tearDown(self):
        self.driver.quit()

if __name__ == "__main__":
    unittest.main()

Pros & Cons of Automation Testing

Pros Cons
Fast execution — thousands of tests in minutes High upfront investment in scripting and setup
Reliable and consistent — no human error during execution Scripts need maintenance as the app changes
Runs unattended — overnight, on every commit Cannot detect UX/usability issues
Scales easily — run on multiple devices in parallel Requires programming knowledge
Integrates with CI/CD for fast feedback loops Flaky tests can erode team confidence if not managed
Excellent for data-driven and regression testing Not ideal for one-off or exploratory tests
Getting Started Tip: Don't try to automate everything at once. Start with your most-run regression tests — the ones your team executes on every release. Get those stable and running in CI first. Once you have a reliable foundation, expand coverage incrementally. A small reliable automation suite is far more valuable than a large flaky one.

Setting Up a Basic Automation Framework

A bare Selenium or Appium script is not a framework — it is a script. A framework adds structure, reusability, and maintainability. Here is the minimal structure I use when starting a new automation project:

project/
├── tests/
│   ├── test_login.py
│   ├── test_checkout.py
│   └── test_search.py
├── pages/                  # Page Object Model classes
│   ├── login_page.py
│   ├── home_page.py
│   └── checkout_page.py
├── utils/
│   ├── driver_factory.py   # WebDriver setup/teardown
│   └── config.py           # URLs, credentials, timeouts
├── test_data/
│   └── users.json          # External test data
├── reports/                # HTML test reports (auto-generated)
├── requirements.txt
└── pytest.ini

Page Object Model (POM)

The Page Object Model is the single most important pattern in test automation. Each page of your application is represented as a Python class. Locators and actions for that page live inside the class — not scattered across test files.

# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LoginPage:
    URL = "https://example.com/login"

    EMAIL_INPUT    = (By.ID, "email")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON   = (By.ID, "loginBtn")
    ERROR_MESSAGE  = (By.CLASS_NAME, "error-message")

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def open(self):
        self.driver.get(self.URL)

    def login(self, email, password):
        self.wait.until(EC.presence_of_element_located(self.EMAIL_INPUT))
        self.driver.find_element(*self.EMAIL_INPUT).send_keys(email)
        self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)
        self.driver.find_element(*self.LOGIN_BUTTON).click()

    def get_error_text(self):
        return self.wait.until(
            EC.presence_of_element_located(self.ERROR_MESSAGE)
        ).text

With POM, when the login button's ID changes, you update it in one place — login_page.py — instead of hunting through dozens of test files. This is the difference between a maintainable suite and a fragile one.

CI/CD Integration

Automation tests only deliver full value when they run automatically on every code change. Here is a minimal GitHub Actions workflow that runs your Selenium suite on every push:

# .github/workflows/test.yml
name: Run Selenium Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Install Chrome
        uses: browser-actions/setup-chrome@latest

      - name: Run tests
        run: pytest tests/ --html=reports/report.html --self-contained-html

      - name: Upload test report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: test-report
          path: reports/report.html

This runs headlessly on Ubuntu with Chrome, generates an HTML report, and uploads it as an artifact — even if tests fail. The if: always() ensures you always get the report regardless of pass/fail.

Managing Test Flakiness

Flaky tests — tests that pass sometimes and fail other times without code changes — are the biggest threat to automation suite credibility. Once the team starts ignoring red builds because "it's probably just flaky," the entire CI safety net collapses.

Root causes of flaky tests

Fixes

From Experience: At Virtusa, our Appium suite went from 30% flakiness to under 5% after three changes: switching from implicit to explicit waits everywhere, replacing XPath locators with resource IDs, and adding a proper setUp/tearDown that reset app state between tests. Flakiness is almost always an engineering problem, not a tooling problem — discipline in locator strategy and wait handling solves the vast majority of cases.

Common Automation Anti-Patterns to Avoid

Most automation projects fail not because of bad tooling, but because of bad engineering habits that compound over time. These are the anti-patterns I see most often — and the ones that consistently turn promising automation suites into maintenance nightmares.

Anti-Pattern Why It's Harmful Better Approach
Automating everything 100% automation coverage is a trap. Writing, maintaining, and debugging an automated test for a one-off scenario or a UI that changes every sprint costs more than executing it manually would have. Apply the automation pyramid: unit and API tests first, UI automation only for stable, high-frequency regression paths. Ask "will this run more than 10 times?" before automating.
Brittle locators XPath like //div[3]/span[1] ties your test to the DOM structure rather than to meaning. A layout change breaks dozens of tests with no change to product behaviour. Use data-testid attributes added specifically for testing. They survive layout changes, are self-documenting, and communicate intent to developers. Agree on the convention with the dev team at project start.
No wait strategy time.sleep(5) makes tests slow on fast machines and still flaky on slow ones. It adds fixed delay regardless of whether the element is already ready or never going to appear. Replace every sleep with an explicit wait: WebDriverWait + ExpectedConditions. The test proceeds the moment the condition is met — faster on fast networks, patient on slow ones.
Shared test state Tests that depend on each other's side effects fail in non-obvious ways when execution order changes (as it does in parallel runs). One failure cascades into dozens of downstream failures. Each test must create its own preconditions and clean up after itself. Use setUp/tearDown or pytest fixtures scoped to the function, not the session.
Automating the wrong layer Using Selenium to test business logic that could be unit-tested is slower, more fragile, and harder to debug. A UI test that takes 30 seconds to validate what a unit test covers in 30 milliseconds is waste. Test at the lowest layer that can meaningfully cover the behaviour. Business logic belongs in unit tests. API contract validation belongs in API tests. Only user journey flows belong in UI automation.
No test maintenance budget Automation written once and never updated becomes unreliable as the product changes. Tests that always fail get ignored. Tests that always pass — even when wrong — give false confidence. Treat automation maintenance as a first-class engineering activity. Allocate sprint capacity for it. Track the ratio of failing tests over time and treat a rising failure rate as a product defect.
Screenshot-only debugging A screenshot of a red screen tells you something failed. It does not tell you which assertion failed, what the actual value was, or what network call preceded the failure. Debugging from screenshots alone is guesswork. Log the actual vs expected values on every assertion failure. Capture logcat or browser console output on failure. Include the full URL, page title, and a stack trace in the failure report. Good failure output is a design requirement, not an afterthought.
Hardcoded environment config Hardcoded URLs, usernames, and passwords in test code mean you cannot run the same suite against staging and production without editing source files. They also end up committed to version control, leaking credentials. Store all environment-specific values in environment variables or a config file excluded from version control. Load them at runtime: os.getenv("BASE_URL"). The same test code then runs against any environment by changing the environment variables, not the code.

The common thread running through every anti-pattern above is the same: short-term convenience traded for long-term cost. Each shortcut feels harmless when you first write it — and the debt accumulates silently until the suite is too slow to run, too flaky to trust, and too brittle to maintain. The discipline to avoid these patterns from day one is what separates an automation suite that lasts from one that gets thrown away and rewritten six months later.


Back to Blog
From Experience — Virtusa: Leading a team of 270 testers at Virtusa, we standardised on Appium for real Android device testing and Selenium WebDriver for web regression. The biggest challenge wasn't the tooling — it was consistency across a team that size. We enforced a strict Page Object Model convention and a pre-merge locator review checklist. Within two sprints, flaky test rates dropped significantly and the team achieved a 20% efficiency gain across regression cycles. At that scale, test architecture decisions matter far more than individual test quality.