Skip to main content
Back to blog

BDD Cucumber Framework Integration with Azure DevOps

How to integrate a BDD Cucumber test framework with Azure DevOps CI/CD. Covers Gherkin scenarios, step definition setup, Cucumber reports, publishing to Azure Test Plans, and running Cucumber tests in Azure Pipelines.

InnovateBits4 min read
Share

BDD (Behaviour-Driven Development) with Cucumber lets non-technical stakeholders write and read test scenarios in plain English. In Azure DevOps, Cucumber tests run as part of CI/CD and publish results alongside other automated tests.


The BDD + Azure DevOps stack

Gherkin feature files (business language)
          ↓
Cucumber (JavaScript/Java/Python) runs scenarios
          ↓
JUnit XML output
          ↓
Azure Pipelines → PublishTestResults
          ↓
Azure Test Plans (results visible)

JavaScript (Playwright + Cucumber)

Feature file

# features/checkout/discount.feature
Feature: Discount code application
 
  Background:
    Given I am logged in as a registered user
    And my cart contains a "Laptop Pro X" worth £899
 
  Scenario: Valid discount code applies correctly
    When I enter discount code "SAVE20"
    And I click "Apply"
    Then I should see "20% discount applied"
    And the order total should be "£719.20"
 
  Scenario: Expired discount code shows error
    When I enter discount code "EXPIRED10"
    And I click "Apply"
    Then I should see error "This discount code has expired"
    And the order total should remain "£899.00"
 
  Scenario Outline: Invalid codes show appropriate errors
    When I enter discount code "<code>"
    And I click "Apply"
    Then I should see error "<message>"
 
    Examples:
      | code     | message                          |
      | INVALID  | Discount code not recognised     |
      | USED123  | This code has already been used  |
      | NOTREAL  | Discount code not recognised     |

Step definitions

// steps/checkout/discount-steps.ts
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
 
Given('I am logged in as a registered user', async function () {
  await this.page.goto('/login')
  await this.page.fill('[name="email"]', process.env.TEST_EMAIL!)
  await this.page.fill('[name="password"]', process.env.TEST_PASSWORD!)
  await this.page.click('[type="submit"]')
  await this.page.waitForURL('**/dashboard')
})
 
When('I enter discount code {string}', async function (code: string) {
  await this.page.fill('[data-testid="discount-input"]', code)
})
 
When('I click {string}', async function (buttonText: string) {
  await this.page.click(`button:has-text("${buttonText}")`)
})
 
Then('I should see {string}', async function (message: string) {
  await expect(this.page.locator('[data-testid="discount-message"]')).toContainText(message)
})
 
Then('the order total should be {string}', async function (total: string) {
  await expect(this.page.locator('[data-testid="order-total"]')).toContainText(total)
})

Cucumber configuration

// cucumber.js
module.exports = {
  default: {
    require: ['steps/**/*.ts', 'support/**/*.ts'],
    requireModule: ['ts-node/register'],
    format: [
      'progress-bar',
      'junit:test-results/cucumber-results.xml',
      'html:test-results/cucumber-report.html',
    ],
    formatOptions: { snippetInterface: 'async-await' },
    paths: ['features/**/*.feature'],
    parallel: 4,
  },
}

Java (Cucumber + Selenium)

Maven dependencies

<dependencies>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>7.18.0</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>7.18.0</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-picocontainer</artifactId>
    <version>7.18.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Runner class

@RunWith(Cucumber.class)
@CucumberOptions(
    features = "src/test/resources/features",
    glue = "steps",
    plugin = {
        "pretty",
        "junit:target/cucumber-results.xml",
        "html:target/cucumber-report.html"
    },
    tags = "@regression"
)
public class CucumberRunner {}

Azure Pipelines YAML

trigger:
  branches:
    include: [main]
 
pool:
  vmImage: ubuntu-latest
 
stages:
  - stage: BDDTests
    displayName: Cucumber BDD Tests
    jobs:
      - job: Cucumber
        timeoutInMinutes: 30
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '20.x'
 
          - script: npm ci
            displayName: Install packages
 
          - script: npx playwright install --with-deps chromium
            displayName: Install Playwright
 
          - script: mkdir -p test-results
            displayName: Create results dir
 
          - script: npx cucumber-js
            displayName: Run Cucumber tests
            env:
              BASE_URL: $(STAGING_URL)
              TEST_EMAIL: $(TEST_EMAIL)
              TEST_PASSWORD: $(TEST_PASSWORD)
            continueOnError: true
 
          - task: PublishTestResults@2
            displayName: Publish Cucumber results
            inputs:
              testResultsFormat: JUnit
              testResultsFiles: test-results/cucumber-results.xml
              testRunTitle: BDD Cucumber — $(Build.BuildNumber)
              mergeTestResults: true
            condition: always()
 
          - task: PublishPipelineArtifact@1
            displayName: Upload HTML report
            inputs:
              targetPath: test-results/cucumber-report.html
              artifact: cucumber-html-report
            condition: always()

Tagging for selective execution

Run only smoke tests in PR pipelines:

- script: npx cucumber-js --tags "@smoke"
  displayName: Smoke BDD tests (PR)
  condition: eq(variables['Build.Reason'], 'PullRequest')
 
- script: npx cucumber-js --tags "@regression and not @wip"
  displayName: Regression BDD tests
  condition: ne(variables['Build.Reason'], 'PullRequest')

Common errors and fixes

Error: Undefined step: Given I am logged in... Fix: The glue path (Java) or require pattern (JS) must match where your step definitions live. Check the path configuration in the runner or cucumber.js config.

Error: Scenario Outline runs but only shows 1 result Fix: Each row in the Examples table is a separate test case. If only 1 appears in results, the JUnit reporter may be collapsing them. Use junit:test-results/results.xml and check the generated XML for multiple <testcase> elements.

Error: Background steps run once for the whole feature, not before each scenario Fix: The Background: section runs before EACH scenario by design. If your background is running only once, check if you've accidentally used BeforeAll instead of Before in hooks.

Error: Parallel execution causes test data collisions Fix: Each parallel worker must create isolated test data. Use unique identifiers (UUID) for any records created during scenario setup.

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