BDD Cucumber + Azure DevOps Integration
How to integrate a BDD Cucumber test framework with Azure DevOps CI/CD. Covers Gherkin scenarios, step definition setup, Cucumber reports, publishing to.
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
GHERKIN1# features/checkout/discount.feature 2Feature: Discount code application 3 4 Background: 5 Given I am logged in as a registered user 6 And my cart contains a "Laptop Pro X" worth £899 7 8 Scenario: Valid discount code applies correctly 9 When I enter discount code "SAVE20" 10 And I click "Apply" 11 Then I should see "20% discount applied" 12 And the order total should be "£719.20" 13 14 Scenario: Expired discount code shows error 15 When I enter discount code "EXPIRED10" 16 And I click "Apply" 17 Then I should see error "This discount code has expired" 18 And the order total should remain "£899.00" 19 20 Scenario Outline: Invalid codes show appropriate errors 21 When I enter discount code "<code>" 22 And I click "Apply" 23 Then I should see error "<message>" 24 25 Examples: 26 | code | message | 27 | INVALID | Discount code not recognised | 28 | USED123 | This code has already been used | 29 | NOTREAL | Discount code not recognised |
Step definitions
TYPESCRIPT1// steps/checkout/discount-steps.ts 2import { Given, When, Then } from '@cucumber/cucumber' 3import { expect } from '@playwright/test' 4 5Given('I am logged in as a registered user', async function () { 6 await this.page.goto('/login') 7 await this.page.fill('[name="email"]', process.env.TEST_EMAIL!) 8 await this.page.fill('[name="password"]', process.env.TEST_PASSWORD!) 9 await this.page.click('[type="submit"]') 10 await this.page.waitForURL('**/dashboard') 11}) 12 13When('I enter discount code {string}', async function (code: string) { 14 await this.page.fill('[data-testid="discount-input"]', code) 15}) 16 17When('I click {string}', async function (buttonText: string) { 18 await this.page.click(`button:has-text("${buttonText}")`) 19}) 20 21Then('I should see {string}', async function (message: string) { 22 await expect(this.page.locator('[data-testid="discount-message"]')).toContainText(message) 23}) 24 25Then('the order total should be {string}', async function (total: string) { 26 await expect(this.page.locator('[data-testid="order-total"]')).toContainText(total) 27})
Cucumber configuration
TYPESCRIPT1// cucumber.js 2module.exports = { 3 default: { 4 require: ['steps/**/*.ts', 'support/**/*.ts'], 5 requireModule: ['ts-node/register'], 6 format: [ 7 'progress-bar', 8 'junit:test-results/cucumber-results.xml', 9 'html:test-results/cucumber-report.html', 10 ], 11 formatOptions: { snippetInterface: 'async-await' }, 12 paths: ['features/**/*.feature'], 13 parallel: 4, 14 }, 15}
Java (Cucumber + Selenium)
Maven dependencies
XML1<dependencies> 2 <dependency> 3 <groupId>io.cucumber</groupId> 4 <artifactId>cucumber-java</artifactId> 5 <version>7.18.0</version> 6 <scope>test</scope> 7 </dependency> 8 <dependency> 9 <groupId>io.cucumber</groupId> 10 <artifactId>cucumber-junit</artifactId> 11 <version>7.18.0</version> 12 <scope>test</scope> 13 </dependency> 14 <dependency> 15 <groupId>io.cucumber</groupId> 16 <artifactId>cucumber-picocontainer</artifactId> 17 <version>7.18.0</version> 18 <scope>test</scope> 19 </dependency> 20</dependencies>
Runner class
JAVA1@RunWith(Cucumber.class) 2@CucumberOptions( 3 features = "src/test/resources/features", 4 glue = "steps", 5 plugin = { 6 "pretty", 7 "junit:target/cucumber-results.xml", 8 "html:target/cucumber-report.html" 9 }, 10 tags = "@regression" 11) 12public class CucumberRunner {}
Azure Pipelines YAML
YAML1trigger: 2 branches: 3 include: [main] 4 5pool: 6 vmImage: ubuntu-latest 7 8stages: 9 - stage: BDDTests 10 displayName: Cucumber BDD Tests 11 jobs: 12 - job: Cucumber 13 timeoutInMinutes: 30 14 steps: 15 - task: NodeTool@0 16 inputs: 17 versionSpec: '20.x' 18 19 - script: npm ci 20 displayName: Install packages 21 22 - script: npx playwright install --with-deps chromium 23 displayName: Install Playwright 24 25 - script: mkdir -p test-results 26 displayName: Create results dir 27 28 - script: npx cucumber-js 29 displayName: Run Cucumber tests 30 env: 31 BASE_URL: $(STAGING_URL) 32 TEST_EMAIL: $(TEST_EMAIL) 33 TEST_PASSWORD: $(TEST_PASSWORD) 34 continueOnError: true 35 36 - task: PublishTestResults@2 37 displayName: Publish Cucumber results 38 inputs: 39 testResultsFormat: JUnit 40 testResultsFiles: test-results/cucumber-results.xml 41 testRunTitle: BDD Cucumber — $(Build.BuildNumber) 42 mergeTestResults: true 43 condition: always() 44 45 - task: PublishPipelineArtifact@1 46 displayName: Upload HTML report 47 inputs: 48 targetPath: test-results/cucumber-report.html 49 artifact: cucumber-html-report 50 condition: always()
Tagging for selective execution
Run only smoke tests in PR pipelines:
YAML1- script: npx cucumber-js --tags "@smoke" 2 displayName: Smoke BDD tests (PR) 3 condition: eq(variables['Build.Reason'], 'PullRequest') 4 5- script: npx cucumber-js --tags "@regression and not @wip" 6 displayName: Regression BDD tests 7 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.
Share this article
Follow for more
Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!