Problem Statement
Explain TDD and BDD practices in CI/CD. Include Red-Green-Refactor cycle, Gherkin syntax, automation strategies, and integration with pipelines.
Explanation
Test-Driven Development (TDD) follows Red-Green-Refactor cycle. Red: write failing test defining expected behavior before implementation. Green: write minimal code passing the test. Refactor: improve code quality while keeping tests passing.
TDD example:
```javascript
// Red: Write failing test
test('calculate total with discount', () => {
const cart = new ShoppingCart();
cart.addItem({ price: 100, quantity: 2 });
expect(cart.getTotal(0.1)).toBe(180); // 10% discount
});
// Green: Minimal implementation
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
getTotal(discount = 0) {
const subtotal = this.items.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
return subtotal * (1 - discount);
}
}
// Refactor: Add validation, error handling
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
if (!item.price || item.price < 0) {
throw new Error('Invalid price');
}
if (!item.quantity || item.quantity < 1) {
throw new Error('Invalid quantity');
}
this.items.push(item);
}
getTotal(discount = 0) {
if (discount < 0 || discount > 1) {
throw new Error('Discount must be between 0 and 1');
}
const subtotal = this.items.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
return subtotal * (1 - discount);
}
}
// Add more tests for edge cases
test('throws error on invalid discount', () => {
const cart = new ShoppingCart();
cart.addItem({ price: 100, quantity: 1 });
expect(() => cart.getTotal(1.5)).toThrow('Discount must be between 0 and 1');
});
```
TDD benefits: better design (tests drive API design), high coverage (tests written first), fewer bugs, confidence in refactoring, living documentation.
Behavior-Driven Development (BDD) extends TDD using business-readable language (Gherkin):
```gherkin
Feature: Shopping Cart
As a customer
I want to manage items in my cart
So that I can purchase products
Background:
Given I am logged in as "customer@example.com"
Scenario: Add item to cart
Given I am on the product page for "iPhone 15"
When I click "Add to Cart"
Then I should see "1 item in cart"
And the cart total should be "$999"
Scenario: Apply discount code
Given I have 1 "iPhone 15" in my cart
When I apply discount code "SAVE10"
Then the cart total should be "$899.10"
And I should see "Discount applied: 10%"
Scenario Outline: Bulk purchase discount
Given I have <quantity> "iPhone 15" in my cart
When I proceed to checkout
Then I should receive <discount>% discount
And the total should be <total>
Examples:
| quantity | discount | total |
| 1 | 0 | $999.00 |
| 5 | 5 | $4745.25 |
| 10 | 10 | $8991.00 |
```
Step definitions implement scenarios:
```javascript
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
Given('I am logged in as {string}', async function (email) {
this.user = await loginUser(email);
});
Given('I am on the product page for {string}', async function (product) {
await this.page.goto(`/products/${product}`);
});
When('I click {string}', async function (buttonText) {
await this.page.click(`button:has-text("${buttonText}")`);
});
Then('I should see {string}', async function (text) {
await expect(this.page.locator('body')).toContainText(text);
});
Then('the cart total should be {string}', async function (amount) {
const total = await this.page.locator('[data-test="cart-total"]').textContent();
expect(total).to.equal(amount);
});
When('I apply discount code {string}', async function (code) {
await this.page.fill('[data-test="discount-code"]', code);
await this.page.click('[data-test="apply-discount"]');
});
```
CI/CD integration:
```yaml
# GitLab CI
stages:
- unit-test
- integration-test
- bdd-test
unit-tests:
stage: unit-test
script:
- npm test
coverage: '/Statements\s*:\s*(\d+\.\d+)%/'
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
integration-tests:
stage: integration-test
services:
- postgres:13
script:
- npm run test:integration
only:
- main
bdd-tests:
stage: bdd-test
script:
- npm run cucumber
artifacts:
when: always
reports:
junit: reports/cucumber-report.xml
paths:
- reports/
only:
- main
- /^release/.*$/
```
GitHub Actions:
```yaml
name: TDD/BDD Pipeline
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm test
- uses: codecov/codecov-action@v3
bdd-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run cucumber
- uses: actions/upload-artifact@v3
if: always()
with:
name: cucumber-report
path: reports/
```
Best practices: write tests first (TDD discipline), keep tests fast, make tests deterministic, use descriptive names, test behaviors not implementation, refactor tests regularly, run TDD unit tests on every commit, run BDD acceptance tests on main branch or before release, enforce coverage thresholds, review tests in pull requests. Understanding TDD/BDD integrates quality into development process from start.