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.1webdriver-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.textUse 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 3Slow 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:
- Headless Chrome configuration with the right flags
webdriver-managerfor automatic driver version management- Explicit waits instead of sleep
- Screenshot capture in conftest.py hooks
PublishTestResultstask withcondition: 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.