Skip to main content
Back to blog

Continuous Testing Strategy in Azure DevOps

How to build a continuous testing strategy in Azure DevOps. Covers test pyramid implementation, pipeline stage design, test selection strategies, environment promotion gates, and quality metrics for continuous delivery.

InnovateBits4 min read
Share

Continuous testing means quality is checked at every stage of the delivery pipeline — not just at the end. In Azure DevOps, this is implemented through layered pipeline stages, each running an appropriate set of tests before allowing progress to the next stage.


The continuous testing pyramid in Azure DevOps

              ▲
             /E2E\          Stage 4: Staging validation
            /─────\         (Playwright, manual smoke)
           / Integ \        Stage 3: Integration tests
          /─────────\       (API, service, contract)
         /   Unit    \      Stage 2: Unit tests
        /─────────────\     (Jest, JUnit, pytest)
       / Static Checks \    Stage 1: Lint, types, SAST
      ───────────────────

Each layer is a stage gate. A failure at any stage stops the pipeline and provides fast feedback to the developer.


Stage 1: Static checks (< 2 minutes)

- stage: StaticChecks
  displayName: Static Analysis
  jobs:
    - job: Checks
      steps:
        - script: npm run lint
        - script: npm run typecheck
        - script: npm audit --audit-level=high   # Security check
        - script: npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause'

Stage 2: Unit tests (< 5 minutes)

- stage: UnitTests
  dependsOn: StaticChecks
  jobs:
    - job: Unit
      steps:
        - script: npm run test:unit -- --coverage --reporter=junit
        - task: PublishTestResults@2
          inputs:
            testResultsFormat: JUnit
            testResultsFiles: results/unit.xml
          condition: always()
        - task: PublishCodeCoverageResults@1
          inputs:
            codeCoverageTool: Cobertura
            summaryFileLocation: coverage/cobertura-coverage.xml

Fail the build if coverage drops below threshold (configured in jest.config.js).


Stage 3: Integration tests (< 10 minutes)

- stage: IntegrationTests
  dependsOn: UnitTests
  jobs:
    - job: Integration
      services:
        postgres: postgres    # Service container
        redis: redis
      steps:
        - script: npm run db:migrate
        - script: npm run test:integration
        - task: PublishTestResults@2
          condition: always()

Stage 4: Deploy to staging + smoke tests (< 15 minutes)

- stage: Staging
  dependsOn: IntegrationTests
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
    - deployment: DeployStaging
      environment: staging
      strategy:
        runOnce:
          deploy:
            steps:
              - script: ./scripts/deploy-staging.sh
 
    - job: SmokeTests
      dependsOn: DeployStaging
      steps:
        - script: npx playwright test --grep @smoke
          env:
            BASE_URL: $(STAGING_URL)
        - task: PublishTestResults@2
          condition: always()

Stage 5: Full regression + approval gate

- stage: RegressionAndApproval
  dependsOn: Staging
  jobs:
    - job: FullRegression
      steps:
        - script: npx playwright test --workers=6
          env:
            BASE_URL: $(STAGING_URL)
        - task: PublishTestResults@2
          condition: always()
 
    - job: QAApproval
      dependsOn: FullRegression
      pool: server    # Human approval — no agent needed
      steps:
        - task: ManualValidation@0
          inputs:
            instructions: |
              Full regression: $(RegressionPassed) tests passed.
              Review the test report before approving production deployment.
              Link: $(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)
            onTimeout: reject
            timeout: 1d   # 24 hours to approve

Test selection strategy

Not all tests should run on every trigger:

TriggerTests to run
Developer PRUnit + lint + smoke (fast)
Merge to mainAll stages
Nightly scheduleFull regression + performance
Pre-releaseFull regression + manual sign-off
# Conditional test selection
- script: npx playwright test --grep $(TEST_GREP)
  env:
    TEST_GREP: ${{ if eq(variables['Build.Reason'], 'PullRequest') }}'@smoke'${{ else }}''${{ end }}

Quality gates for release

Before production deployment, verify:

- script: |
    PASS_RATE=$(cat test-results/metrics.json | jq '.passRate')
    echo "Pass rate: $PASS_RATE%"
    if (( $(echo "$PASS_RATE < 95" | bc -l) )); then
      echo "##vso[task.logissue type=error]Pass rate $PASS_RATE% is below 95% threshold"
      exit 1
    fi
  displayName: Quality gate check

Common errors and fixes

Error: Integration tests fail because database isn't ready Fix: Use service container health checks. The job won't start until the health check passes. Add --health-cmd, --health-interval, --health-retries to the container options.

Error: Manual approval gate expires during long weekends Fix: Set timeout to at least 72 hours for weekends. Also configure notifications so approvers are alerted immediately when the gate opens.

Error: Regression stage runs even when staging deployment failed Fix: Use dependsOn: DeployStaging with condition: succeeded() on the regression job. Without the condition, the job may still run when the deployment job fails.

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