Testing in Azure DevOps Pipelines
A full YAML pipeline with stages, jobs, parallel test slicing and the best built-in test reporting of any CI platform.
1 min read · updated 19 September 2026
Azure Pipelines is the enterprise .NET default, and its test reporting is genuinely the best of the platforms covered here — published results feed Test Analytics, which tracks per-test pass rate, duration and flakiness over time without any extra tooling.
#A complete pipeline
# azure-pipelines.yml
trigger:
branches: { include: [main] }
pr:
branches: { include: [main] }
variables:
buildConfiguration: 'Release'
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
stages:
# ---------------------------------------------------------------- #
- stage: Build
jobs:
- job: Compile
pool: { vmImage: 'ubuntu-latest' }
steps:
- task: UseDotNet@2
inputs: { version: '9.x' }
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
restoreKeys: 'nuget | "$(Agent.OS)"'
path: $(NUGET_PACKAGES)
- script: dotnet build --configuration $(buildConfiguration)
displayName: Build
# ---------------------------------------------------------------- #
- stage: Test
dependsOn: Build
jobs:
- job: Unit
pool: { vmImage: 'ubuntu-latest' }
steps:
- task: UseDotNet@2
inputs: { version: '9.x' }
- script: |
dotnet test \
--configuration $(buildConfiguration) \
--filter "Category!=Integration" \
--logger "trx;LogFileName=unit.trx" \
--collect:"XPlat Code Coverage" \
--results-directory $(Agent.TempDirectory)/TestResults
displayName: Unit tests
# condition: always() — a report is needed most when the step failed
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: VSTest
testResultsFiles: '**/unit.trx'
testRunTitle: 'Unit tests'
failTaskOnFailedTests: true
- task: PublishCodeCoverageResults@2
condition: always()
inputs:
summaryFileLocation: '$(Agent.TempDirectory)/TestResults/**/coverage.cobertura.xml'
- job: Integration
pool: { vmImage: 'ubuntu-latest' }
steps:
- task: UseDotNet@2
inputs: { version: '9.x' }
# The hosted Ubuntu agent has Docker, so Testcontainers just works.
- script: dotnet test --filter "Category=Integration" --logger "trx;LogFileName=integration.trx"
displayName: Integration tests
- task: PublishTestResults@2
condition: always()
inputs: { testResultsFormat: VSTest, testResultsFiles: '**/integration.trx' }
- job: E2E
pool: { vmImage: 'ubuntu-latest' }
# Four agents run this job; each slices the suite itself.
strategy:
parallel: 4
steps:
- task: NodeTool@0
inputs: { versionSpec: '22.x' }
- script: npm ci && npx playwright install --with-deps chromium
- script: |
npx playwright test \
--shard=$((System.JobPositionInPhase))/$((System.TotalJobsInPhase)) \
--reporter=junit,blob
displayName: Playwright
env:
PLAYWRIGHT_JUNIT_OUTPUT_NAME: results.xml
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: JUnit
testResultsFiles: 'results.xml'
testRunTitle: 'E2E shard $(System.JobPositionInPhase)'
- task: PublishPipelineArtifact@1
condition: always()
inputs:
targetPath: 'blob-report'
artifact: 'blob-$(System.JobPositionInPhase)'
# ---------------------------------------------------------------- #
- stage: Deploy
dependsOn: Test
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: Production
environment: production # approvals and gates attach here
strategy:
runOnce:
deploy:
steps:
- script: ./deploy.sh#Test slicing
Two mechanisms, depending on the runner.
Manual slicing, as above: parallel: 4 starts four agents and sets
System.JobPositionInPhase (1-based) and System.TotalJobsInPhase. You
pass them to whatever your runner's shard flag is. This is what
Playwright sharding needs.
Automatic slicing with VSTest for .NET:
- task: VSTest@3
inputs:
testSelector: testAssemblies
testAssemblyVer2: '**/*Tests.dll'
runInParallel: true
# Slice by previous run time — the best distribution available here.
distributionBatchType: basedOnTestCases
rerunFailedTests: true
rerunMaxAttempts: 2rerunFailedTests with Test Analytics is how Azure DevOps does
flaky detection: a test that fails and then passes
on rerun is marked flaky rather than counted as a pass, and it appears in
the analytics view with a history.
That is the feature worth choosing this platform for.
#Test Analytics
Once PublishTestResults has run over several builds, the Analytics view
gives you, per test:
- pass rate over time
- duration trend
- flakiness classification
- failure frequency by branch
The two reports that change behaviour are "top failing tests" and "longest running tests". Both are the input to a sensible weekly tidy-up, and neither requires any instrumentation beyond publishing results.
#Service containers
resources:
containers:
- container: postgres
image: postgres:16-alpine
ports: ['5432:5432']
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
jobs:
- job: Integration
services:
postgres: postgres
steps:
- script: dotnet test
env:
ConnectionStrings__Default: 'Host=localhost;Database=test;Username=postgres;Password=test'As elsewhere, Testcontainers is usually preferable — the dependency is declared in the test code and behaves identically on a developer machine.
#Templates
The feature that keeps a large organisation's pipelines consistent:
# templates/dotnet-test.yml
parameters:
- name: filter
type: string
default: ''
- name: title
type: string
steps:
- script: dotnet test --filter "${{ parameters.filter }}" --logger "trx;LogFileName=${{ parameters.title }}.trx"
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: VSTest
testResultsFiles: '**/${{ parameters.title }}.trx'
testRunTitle: ${{ parameters.title }}# azure-pipelines.yml
- template: templates/dotnet-test.yml
parameters: { filter: 'Category!=Integration', title: 'Unit' }#Compared with GitHub Actions
Azure DevOps has better test reporting and analytics, first-class environments with approvals, and templates that scale across many repositories. GitHub Actions has a far larger action ecosystem, simpler YAML, and tighter integration with pull requests.
For a .NET shop already inside Azure DevOps, the reporting alone is a good reason to stay.
Common questions
- What does Azure DevOps do better than other CI platforms for testing?
- Test reporting. Results published with PublishTestResults feed Test Analytics, which tracks pass rate, duration and flakiness per test over time and surfaces the worst offenders automatically. No other platform here gives you that without extra tooling.
- How do I run tests in parallel in Azure Pipelines?
- parallel in the job strategy gives you N agents running the same job with System.JobPositionInPhase and System.TotalJobsInPhase set, and you slice the suite yourself. For .NET, VSTest can slice automatically with distributionBatchType.
- How do I detect flaky tests in Azure DevOps?
- Test Analytics flags them automatically once results are published over several runs, and there is a built-in flaky test detection setting that reruns failures and marks tests that pass on retry.
Runnable samples for this page
last test results ↗- YAML / Groovy / Kotlin
pipelines/ci-cd/azure-devops-pipelines
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Testing in GitHub ActionsA complete pipeline — unit tests, integration with containers, sharded Playwright, coverage and artefacts — plus the caching and concurrency settings that make it fast.
- Testing in TeamCityBuild chains, real-time test reporting and the best flaky-test detection of any CI platform — configured as Kotlin DSL rather than clicked together.
- Writing Tests in C#The .NET testing stack — xUnit, Moq or NSubstitute, WebApplicationFactory, Testcontainers — and the dependency injection story that makes it the most testable of the four.
- Test ReportingGetting results out of the runner and in front of people — JUnit XML, pull request annotations, per-test history, and the numbers worth publishing.
- Code CoverageWhat the percentage measures, why it is a finding tool rather than a target, how to collect it in each ecosystem, and how to gate on it without causing harm.
- MSTestMicrosoft's own .NET test framework — where it sits against xUnit and NUnit, what MSTest 3 improved, and when it is the right organisational choice.