DevOps 4 min read

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.

I
InnovateBits
InnovateBits

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

GHERKIN
1# 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

TYPESCRIPT
1// 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

TYPESCRIPT
1// 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

XML
1<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

JAVA
1@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

YAML
1trigger: 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:

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

Tags
#bdd#cucumber#gherkin#azure-devops#test-automation#azure-pipelines#behavior-driven-development

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!