How to Create Azure DevOps Pipeline for Automated Testing
Step-by-step guide to creating an Azure DevOps pipeline for automated testing. Covers pipeline creation, YAML configuration, test result publishing, artifact storage, and pipeline triggers — with complete working examples.
Creating a pipeline for automated testing in Azure DevOps involves three decisions: what triggers the pipeline, what steps it runs, and how it reports results. This guide walks through each decision with complete, working YAML.
Creating the pipeline
Option A: From the UI (YAML)
- Go to Pipelines → + New pipeline
- Select your source: Azure Repos Git (or GitHub)
- Select your repository
- Choose Starter pipeline (edits in the browser) or Existing Azure Pipelines YAML file (if you already have a YAML file)
Option B: Commit azure-pipelines.yml
Create the file at the root of your repository:
touch azure-pipelines.yml
git add azure-pipelines.yml
git commit -m "Add CI pipeline"
git pushAzure DevOps detects this file automatically when you create the pipeline.
Pipeline structure for testing
A well-structured testing pipeline has three stages:
Stage 1: Validate (fast checks — linting, compilation)
Stage 2: Unit Tests (fast, isolated tests — run first)
Stage 3: Integration/E2E (slower, environment-dependent tests)
Failing fast in Stage 1 saves agent minutes and developer waiting time.
Complete pipeline YAML
# azure-pipelines.yml
trigger:
branches:
include:
- main
- release/*
paths:
exclude:
- docs/**
- '*.md'
pr:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
variables:
- group: test-environment-vars # Variable group from Library
stages:
# ── Stage 1: Validate ─────────────────────────────────────────────────────
- stage: Validate
displayName: Validate
jobs:
- job: Lint
displayName: Lint and type check
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: Install dependencies
- script: npm run lint
displayName: ESLint
- script: npm run typecheck
displayName: TypeScript check
# ── Stage 2: Unit Tests ───────────────────────────────────────────────────
- stage: UnitTests
displayName: Unit Tests
dependsOn: Validate
jobs:
- job: Unit
displayName: Run unit tests
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: Install
- script: |
npm run test:unit -- \
--reporter=junit \
--outputFile=results/unit-results.xml \
--coverage
displayName: Run unit tests
- task: PublishTestResults@2
displayName: Publish unit results
inputs:
testResultsFormat: JUnit
testResultsFiles: results/unit-results.xml
testRunTitle: Unit Tests — $(Build.BuildNumber)
mergeTestResults: true
condition: always()
- task: PublishCodeCoverageResults@1
displayName: Publish coverage
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: coverage/cobertura-coverage.xml
condition: always()
# ── Stage 3: E2E Tests ────────────────────────────────────────────────────
- stage: E2ETests
displayName: E2E Tests
dependsOn: UnitTests
condition: succeeded()
jobs:
- job: Playwright
displayName: Playwright E2E
timeoutInMinutes: 30
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm ci
displayName: Install
- script: npx playwright install --with-deps chromium firefox
displayName: Install browsers
- script: npx playwright test --reporter=junit,html
displayName: Run Playwright tests
env:
BASE_URL: $(STAGING_URL)
TEST_EMAIL: $(TEST_USER_EMAIL)
TEST_PASSWORD: $(TEST_USER_PASSWORD)
continueOnError: false
- task: PublishTestResults@2
displayName: Publish E2E results
inputs:
testResultsFormat: JUnit
testResultsFiles: playwright-results/results.xml
testRunTitle: E2E Tests — $(Build.BuildNumber)
condition: always()
- task: PublishPipelineArtifact@1
displayName: Upload HTML report
inputs:
targetPath: playwright-report
artifact: playwright-html-report
publishLocation: pipeline
condition: always()
- task: PublishPipelineArtifact@1
displayName: Upload screenshots on failure
inputs:
targetPath: test-results
artifact: test-screenshots
condition: failed()Setting up variables and secrets
Never hardcode credentials in YAML. Use variable groups:
- Go to Pipelines → Library → + Variable group
- Name:
test-environment-vars - Add variables:
STAGING_URL = https://staging.yourapp.com TEST_USER_EMAIL = testuser@example.com TEST_USER_PASSWORD = [mark as secret] **** - Reference the group in pipeline YAML:
- group: test-environment-vars
Secret variables are masked in logs automatically.
Pull request validation
To block merges when tests fail:
- Go to Repos → Branches → [main branch] → Branch Policies
- Click + Add build validation
- Select your pipeline
- Set: Required (not Optional)
- Trigger: Automatic on PR updates
Now PRs to main cannot be merged until the pipeline passes.
Common errors and fixes
Error: Pipeline runs but no test results appear in the Tests tab
Fix: The PublishTestResults task must find actual XML files. Add a debug step: - script: find $(System.DefaultWorkingDirectory) -name "*.xml" -type f to see what's generated.
Error: "The pipeline is not valid. Job 'E2E' has a dependency on 'UnitTests' which doesn't exist"
Fix: dependsOn references the stage name (not displayName). Match it exactly: dependsOn: UnitTests.
Error: E2E tests time out after 10 minutes
Fix: Add timeoutInMinutes: 30 (or appropriate value) to the job definition. Default timeout is 60 minutes for Microsoft-hosted agents, but PR validation pipelines have a 10-minute default.
Error: Tests fail with "ECONNREFUSED" — can't reach the application Fix: The staging environment URL is unreachable from the pipeline agent. Check if staging is behind a VPN or firewall that blocks Azure DevOps agents. Use self-hosted agents inside the network if needed.
Error: Variable group not found in pipeline Fix: Go to Library → [Variable group] → Pipeline permissions and authorize the pipeline to use the group.
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