Run Selenium Tests in Azure DevOps Pipeline: Step-by-Step YAML
Complete guide to running Selenium tests in Azure DevOps pipelines. Covers Java and Python examples, ChromeDriver setup on Linux agents, headless browser configuration, test result publishing, and parallel execution.
Running Selenium tests in Azure DevOps requires configuring headless browsers on the agent, setting up the correct WebDriver, and publishing test results in a format Azure DevOps understands. This guide covers complete working YAML for both Java (TestNG/JUnit) and Python (pytest) setups.
Prerequisites on the agent
Microsoft-hosted Ubuntu agents include:
- Chrome browser (regularly updated)
- ChromeDriver matching the Chrome version
- Java 17
- Python 3.10+
- Maven, Gradle
They do NOT include a display server, so all browser tests must run in headless mode.
Java + TestNG pipeline
Project structure
selenium-tests/
pom.xml
src/test/java/
tests/
LoginTest.java
CheckoutTest.java
testng.xml
pom.xml dependencies
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.21.0</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>Headless Chrome setup in tests
// BaseTest.java
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
public class BaseTest {
protected WebDriver driver;
@BeforeMethod
public void setUp() {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new"); // Headless mode for CI
options.addArguments("--no-sandbox"); // Required in Docker/CI
options.addArguments("--disable-dev-shm-usage"); // Prevent memory issues
options.addArguments("--window-size=1920,1080");
driver = new ChromeDriver(options);
}
@AfterMethod
public void tearDown() {
if (driver != null) driver.quit();
}
}Azure Pipelines YAML — Java
trigger:
branches:
include: [main]
pool:
vmImage: ubuntu-latest
stages:
- stage: SeleniumTests
displayName: Selenium Tests
jobs:
- job: TestNG
displayName: TestNG Selenium Suite
timeoutInMinutes: 30
steps:
- task: JavaToolInstaller@0
displayName: Setup Java 17
inputs:
versionSpec: '17'
jdkArchitectureOption: x64
jdkSourceOption: PreInstalled
- script: mvn --version && java --version
displayName: Verify Java setup
- script: |
mvn test \
-Dsurefire.rerunFailingTestsCount=1 \
-DBASE_URL=$(STAGING_URL)
displayName: Run Selenium tests
env:
STAGING_URL: $(STAGING_URL)
- task: PublishTestResults@2
displayName: Publish TestNG results
inputs:
testResultsFormat: JUnit
testResultsFiles: '**/surefire-reports/TEST-*.xml'
testRunTitle: Selenium TestNG — $(Build.BuildNumber)
mergeTestResults: true
condition: always()
- task: PublishPipelineArtifact@1
displayName: Upload screenshots on failure
inputs:
targetPath: test-screenshots
artifact: selenium-screenshots
condition: failed()Python + pytest pipeline
Requirements
# requirements.txt
selenium==4.21.0
pytest==8.2.0
pytest-html==4.1.1
pytest-xdist==3.5.0 # For parallel execution
Headless Chrome in Python
# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
@pytest.fixture(scope="function")
def driver():
options = Options()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--window-size=1920,1080")
service = Service() # WebDriverManager or system ChromeDriver
browser = webdriver.Chrome(service=service, options=options)
yield browser
browser.quit()Azure Pipelines YAML — Python
trigger:
branches:
include: [main]
pool:
vmImage: ubuntu-latest
stages:
- stage: SeleniumTests
jobs:
- job: Pytest
timeoutInMinutes: 30
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
- script: pip install -r requirements.txt
displayName: Install dependencies
- script: |
pytest tests/ \
--junitxml=test-results/results.xml \
--html=test-results/report.html \
--self-contained-html \
-n auto \
-v
displayName: Run Selenium tests
env:
BASE_URL: $(STAGING_URL)
- task: PublishTestResults@2
displayName: Publish pytest results
inputs:
testResultsFormat: JUnit
testResultsFiles: test-results/results.xml
testRunTitle: Selenium pytest — $(Build.BuildNumber)
condition: always()
- task: PublishPipelineArtifact@1
inputs:
targetPath: test-results/report.html
artifact: pytest-html-report
condition: always()Common errors and fixes
Error: DevToolsActivePort file doesn't exist in Chrome
Fix: Add --no-sandbox and --disable-dev-shm-usage Chrome options. This error occurs in Linux environments without proper Chrome sandbox support.
Error: ChromeDriver only supports Chrome version 114 — version mismatch
Fix: Use WebDriverManager (io.github.bonigarcia:webdrivermanager) in Java or webdriver-manager in Python — it automatically downloads the matching ChromeDriver version.
Error: Tests pass locally but fail in pipeline with "element not visible"
Fix: Headless Chrome may render differently. Set explicit window size (--window-size=1920,1080) and add explicit waits (WebDriverWait) instead of Thread.sleep().
Error: Screenshots directory not found when trying to upload artifact
Fix: Create the directory unconditionally: - script: mkdir -p test-screenshots before tests run. Upload artifacts with condition: always() not condition: failed().
Error: Maven build fails: "Tests run: 0, Failures: 0, Errors: 0, Skipped: 0"
Fix: Check that test class names match the Surefire plugin pattern (default: Test*.java, *Test.java, *Tests.java). Add <includes><include>**/*Test.java</include></includes> to Surefire plugin config if using a different pattern.
Stay ahead in AI-driven QA
Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.
Discussion
Sign in with GitHub to comment · powered by Giscus