Azure DevOps Selenium Integration: Step-by-Step Tutorial

Azure DevOps Selenium Integration: Step-by-Step Tutorial

Azure DevOps Selenium integration lets you run browser-based tests automatically on every commit. This tutorial covers everything from initial setup to running Selenium tests reliably in Azure Pipelines, including headless execution, result publishing, and common failure modes.

Prerequisites

Before integrating Selenium with Azure DevOps, you need:

  • An Azure DevOps organization and project
  • A test project using Selenium WebDriver (Python, Java, or C#)
  • A pipeline YAML file in your repository

This tutorial uses Python with pytest-selenium, but the Azure Pipelines configuration applies to any Selenium setup.

Why Selenium in Azure DevOps?

Selenium is the most widely used browser automation framework, and Azure DevOps provides the CI/CD infrastructure to run it at scale. The combination gives you:

  • Automated browser tests on every pull request
  • Cross-browser testing with matrix strategies
  • Integrated test result reporting
  • Artifact storage for screenshots and logs on failure

The main challenge is browser setup — Azure DevOps hosted agents come with Chrome installed, but you need to configure headless mode and ensure your WebDriver version matches the installed browser.

Step 1: Install Dependencies

Start with your requirements.txt:

selenium==4.18.1
pytest==8.0.0
pytest-selenium==4.1.0
webdriver-manager==4.0.1

webdriver-manager handles matching ChromeDriver versions to the installed Chrome — eliminates one of the most common CI failure causes.

Step 2: Configure Selenium for CI

Your test code needs to behave differently in CI vs locally. Detect the CI environment and configure accordingly:

import os
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

@pytest.fixture(scope="session")
def driver():
    options = Options()

    # Always headless in CI
    if os.environ.get('CI') or os.environ.get('TF_BUILD'):
        options.add_argument('--headless')
        options.add_argument('--no-sandbox')
        options.add_argument('--disable-dev-shm-usage')
        options.add_argument('--disable-gpu')
        options.add_argument('--window-size=1920,1080')

    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    driver.implicitly_wait(10)

    yield driver
    driver.quit()

TF_BUILD is the Azure Pipelines environment variable that indicates you're running in a pipeline. --no-sandbox and --disable-dev-shm-usage are required for Chrome in Linux containers.

Step 3: Write Your Tests

A basic Selenium test with proper waits:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class TestLogin:
    def test_successful_login(self, driver):
        driver.get("https://yourapp.com/login")

        wait = WebDriverWait(driver, 10)

        email = wait.until(EC.presence_of_element_located((By.ID, "email")))
        email.send_keys("test@example.com")

        password = driver.find_element(By.ID, "password")
        password.send_keys("testpassword")

        driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

        # Assert redirect to dashboard
        wait.until(EC.url_contains("/dashboard"))
        assert "/dashboard" in driver.current_url

    def test_failed_login_shows_error(self, driver):
        driver.get("https://yourapp.com/login")

        wait = WebDriverWait(driver, 10)

        email = wait.until(EC.presence_of_element_located((By.ID, "email")))
        email.send_keys("wrong@example.com")

        driver.find_element(By.ID, "password").send_keys("wrongpassword")
        driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

        error = wait.until(EC.presence_of_element_located((By.CLASS_NAME, "error-message")))
        assert "Invalid credentials" in error.text

Use WebDriverWait with explicit conditions rather than time.sleep(). Explicit waits make tests faster and more reliable.

Step 4: Configure Azure Pipelines

Now wire this into Azure Pipelines:

trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

variables:
  APP_URL: 'https://staging.yourapp.com'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '3.11'
    displayName: 'Set Python version'

  - script: |
      pip install -r requirements.txt
    displayName: 'Install dependencies'

  - script: |
      pytest tests/e2e/ \
        --junitxml=$(Build.ArtifactStagingDirectory)/selenium-results.xml \
        -v \
        --tb=short
    displayName: 'Run Selenium tests'
    env:
      APP_URL: $(APP_URL)
      TF_BUILD: '1'

  - task: PublishTestResults@2
    inputs:
      testResultsFormat: 'JUnit'
      testResultsFiles: '$(Build.ArtifactStagingDirectory)/selenium-results.xml'
      testRunTitle: 'Selenium Tests - $(Build.BuildNumber)'
      failTaskOnFailedTests: true
    condition: always()
    displayName: 'Publish test results'

The condition: always() on the publish step is critical — it ensures results appear even when tests fail, so you can diagnose what went wrong.

Step 5: Capture Screenshots on Failure

Selenium tests fail silently without screenshots. Add a conftest.py fixture that captures them:

import pytest
import os
from datetime import datetime

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()

    if rep.when == "call" and rep.failed:
        driver = item.funcargs.get("driver")
        if driver:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            screenshot_dir = os.environ.get(
                'BUILD_ARTIFACTSTAGINGDIRECTORY', 'screenshots'
            )
            os.makedirs(screenshot_dir, exist_ok=True)
            screenshot_path = f"{screenshot_dir}/failure_{item.name}_{timestamp}.png"
            driver.save_screenshot(screenshot_path)

Then publish screenshots as artifacts:

- task: PublishBuildArtifacts@1
  inputs:
    pathToPublish: '$(Build.ArtifactStagingDirectory)'
    artifactName: 'test-artifacts'
  condition: always()
  displayName: 'Publish screenshots and logs'

Screenshots appear in the pipeline run under Published Artifacts — invaluable for debugging CI failures.

Step 6: Cross-Browser Testing

Run Selenium tests across multiple browsers using matrix strategy:

jobs:
  - job: SeleniumTests
    strategy:
      matrix:
        Chrome:
          BROWSER: chrome
        Firefox:
          BROWSER: firefox
      maxParallel: 2

    steps:
      - task: UsePythonVersion@0
        inputs:
          versionSpec: '3.11'

      - script: pip install -r requirements.txt
        displayName: 'Install dependencies'

      - script: |
          pytest tests/e2e/ \
            --browser=$(BROWSER) \
            --junitxml=results-$(BROWSER).xml
        displayName: 'Run tests on $(BROWSER)'

      - task: PublishTestResults@2
        inputs:
          testResultsFiles: 'results-$(BROWSER).xml'
          testRunTitle: 'Selenium $(BROWSER) Tests'
        condition: always()

Firefox needs additional setup on Ubuntu:

- script: |
    sudo apt-get update
    sudo apt-get install -y firefox-esr
    pip install webdriver-manager
  displayName: 'Install Firefox'

Common Azure DevOps Selenium Failures

ChromeDriver version mismatch: Use webdriver-manager to auto-match versions. Don't pin ChromeDriver manually.

Display not found: Always use --headless in Linux agents. Add --disable-gpu for older Chrome versions.

Tests passing locally but failing in CI: Usually timing issues. Switch from implicit waits to explicit WebDriverWait. Add --window-size=1920,1080 to ensure consistent rendering.

Flaky tests: Add retry logic with pytest-rerunfailures:

pip install pytest-rerunfailures
pytest --reruns 2 --reruns-delay 3

Slow test runs: Cache pip packages, use parallel matrix, and limit E2E tests to the main branch for faster PR feedback.

Scaling Beyond Selenium

As your test suite grows, managing ChromeDriver versions, headless configuration, and browser infrastructure in Azure Pipelines becomes overhead. Some teams move to cloud-hosted browser testing platforms.

HelpMeTest runs Playwright tests (a modern Selenium alternative) in managed cloud infrastructure — no ChromeDriver setup, no headless configuration, no screenshot handling. With usage-based pricing and no base fee, it's an alternative worth evaluating when your team is spending more time on infrastructure than on tests themselves. It integrates with Azure Pipelines via API so your pipeline still orchestrates the overall flow.

Summary

Azure DevOps Selenium integration requires:

  1. Headless Chrome configuration with the right flags
  2. webdriver-manager for automatic driver version management
  3. Explicit waits instead of sleep
  4. Screenshot capture in conftest.py hooks
  5. PublishTestResults task with condition: always()

Get these right and your Selenium tests will run reliably in Azure Pipelines with full reporting and artifact storage.

Try HelpMeTest if you want to skip the browser infrastructure setup and focus on writing tests.

Read more

Start now free