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.
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.
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