Skip to main content
Back to blog

Selenium Framework Integration with Azure DevOps: Complete Setup

How to integrate a Selenium test automation framework with Azure DevOps. Covers Maven/Gradle builds, TestNG/JUnit XML output, ChromeDriver configuration for CI, WebDriverManager setup, and publishing results to Azure Test Plans.

InnovateBits4 min read
Share

A Selenium framework integrated with Azure DevOps runs automatically on every commit, publishes results to Azure Test Plans, and flags regressions before they reach production. This guide covers a complete, production-ready integration.


Framework structure

selenium-framework/
├── pom.xml                          # Maven build file
├── azure-pipelines.yml              # Pipeline definition
├── testng.xml                       # TestNG suite configuration
├── src/
│   ├── main/java/
│   │   └── framework/
│   │       ├── BaseTest.java        # WebDriver lifecycle
│   │       ├── DriverFactory.java   # Driver initialisation
│   │       └── pages/              # Page Object Model
│   │           ├── LoginPage.java
│   │           └── DashboardPage.java
│   └── test/java/
│       └── tests/
│           ├── AuthTests.java
│           └── CheckoutTests.java
└── src/test/resources/
    ├── config.properties            # Environment config
    └── test-data.json               # Test data

Maven configuration (pom.xml)

<project>
  <properties>
    <selenium.version>4.21.0</selenium.version>
    <testng.version>7.10.2</testng.version>
    <wdm.version>5.8.0</wdm.version>
    <maven.surefire.version>3.2.5</maven.surefire.version>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
    </dependency>
    <dependency>
      <groupId>io.github.bonigarcia</groupId>
      <artifactId>webdrivermanager</artifactId>
      <version>${wdm.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>${testng.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>${maven.surefire.version}</version>
        <configuration>
          <suiteXmlFiles>
            <suiteXmlFile>testng.xml</suiteXmlFile>
          </suiteXmlFiles>
          <rerunFailingTestsCount>1</rerunFailingTestsCount>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

BaseTest with CI-aware configuration

public class BaseTest {
    protected WebDriver driver;
    protected String baseUrl;
 
    @BeforeMethod
    @Parameters({"browser"})
    public void setUp(@Optional("chrome") String browser) {
        baseUrl = System.getenv("BASE_URL") != null
            ? System.getenv("BASE_URL")
            : "http://localhost:3000";
 
        boolean isCI = System.getenv("TF_BUILD") != null; // Azure Pipelines sets TF_BUILD
 
        if (browser.equals("chrome")) {
            WebDriverManager.chromedriver().setup();
            ChromeOptions options = new ChromeOptions();
            if (isCI) {
                options.addArguments("--headless=new", "--no-sandbox",
                    "--disable-dev-shm-usage", "--window-size=1920,1080");
            }
            driver = new ChromeDriver(options);
        } else if (browser.equals("firefox")) {
            WebDriverManager.firefoxdriver().setup();
            FirefoxOptions ffOptions = new FirefoxOptions();
            if (isCI) ffOptions.addArguments("--headless");
            driver = new FirefoxDriver(ffOptions);
        }
 
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
    }
 
    @AfterMethod(alwaysRun = true)
    public void tearDown(ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE && driver != null) {
            captureScreenshot(result.getName());
        }
        if (driver != null) driver.quit();
    }
 
    private void captureScreenshot(String testName) {
        TakesScreenshot ts = (TakesScreenshot) driver;
        File src = ts.getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(src, new File("test-screenshots/" + testName + ".png"));
        } catch (IOException e) {
            System.err.println("Screenshot failed: " + e.getMessage());
        }
    }
}

TestNG suite configuration

<!-- testng.xml -->
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression Suite" parallel="classes" thread-count="3">
    <listeners>
        <listener class-name="org.testng.reporters.JUnitXMLReporter"/>
    </listeners>
    <test name="Auth Tests">
        <parameter name="browser" value="chrome"/>
        <classes>
            <class name="tests.AuthTests"/>
        </classes>
    </test>
    <test name="Checkout Tests">
        <parameter name="browser" value="chrome"/>
        <classes>
            <class name="tests.CheckoutTests"/>
        </classes>
    </test>
</suite>

Azure Pipelines YAML

trigger:
  branches:
    include: [main, release/*]
 
pool:
  vmImage: ubuntu-latest
 
variables:
  - group: selenium-env-vars
  - name: MAVEN_CACHE_FOLDER
    value: $(Pipeline.Workspace)/.m2/repository
  - name: MAVEN_OPTS
    value: -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)
 
stages:
  - stage: SeleniumRegression
    displayName: Selenium Regression Suite
    jobs:
      - job: TestNG
        displayName: TestNG Selenium Tests
        timeoutInMinutes: 45
        steps:
          - task: Cache@2
            displayName: Cache Maven packages
            inputs:
              key: 'maven | "$(Agent.OS)" | **/pom.xml'
              restoreKeys: 'maven | "$(Agent.OS)"'
              path: $(MAVEN_CACHE_FOLDER)
 
          - task: JavaToolInstaller@0
            displayName: Set up Java 17
            inputs:
              versionSpec: '17'
              jdkArchitectureOption: x64
              jdkSourceOption: PreInstalled
 
          - script: mkdir -p test-screenshots test-results
            displayName: Create output directories
 
          - script: |
              mvn test \
                -Dsurefire.rerunFailingTestsCount=1 \
                -Dmaven.test.failure.ignore=true \
                $(MAVEN_OPTS)
            displayName: Run Selenium tests
            env:
              BASE_URL: $(STAGING_URL)
              TF_BUILD: $(TF_BUILD)
 
          - 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
            inputs:
              targetPath: test-screenshots
              artifact: selenium-screenshots
            condition: always()

Common errors and fixes

Error: java.lang.UnsatisfiedLinkError for ChromeDriver on Linux Fix: Add --no-sandbox and --disable-dev-shm-usage Chrome options. On Azure Linux agents, the sandbox requires privileges that CI environments don't have.

Error: Maven downloads all dependencies on every pipeline run (slow) Fix: Add the Cache@2 task to cache the .m2 repository. The cache key based on pom.xml means it only invalidates when dependencies change.

Error: TestNG reports show 0 tests even though tests ran Fix: The JUnit XML reporter in TestNG generates to surefire-reports by default. Verify the path with find . -name "TEST-*.xml" in a script step.

Error: WebDriverManager failed to download ChromeDriver Fix: Azure agents have Chrome pre-installed. Use WebDriverManager.chromedriver().browserVersion("installed").setup() to use the agent's installed Chrome version without downloading.

Free newsletter

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