BDD with Cucumber - 行为驱动开发实践

全面的 Cucumber BDD 示例,包括功能文件、步骤定义、数据表格、钩子和高级 BDD 模式,用于协作开发

💻 Cucumber BDD 基础设置和配置 javascript

🟢 simple ⭐⭐

完整的 Cucumber 项目设置,包含功能文件、步骤定义、配置和基础 BDD 模式

⏱️ 45 min 🏷️ cucumber, bdd, setup, configuration
Prerequisites: JavaScript basics, Testing concepts, Gherkin syntax
// Cucumber BDD - Basic Setup and Configuration

// 1. package.json - Dependencies and Scripts
{
  "name": "cucumber-bdd-example",
  "version": "1.0.0",
  "description": "Behavior Driven Development with Cucumber",
  "main": "index.js",
  "scripts": {
    "test": "cucumber-js",
    "test:parallel": "cucumber-js --parallel 4",
    "test:wip": "cucumber-js --tags @wip",
    "test:smoke": "cucumber-js --tags @smoke",
    "test:regression": "cucumber-js --tags @regression",
    "report:html": "cucumber-js --format html:reports/cucumber-report.html",
    "report:json": "cucumber-js --format json:reports/cucumber-report.json"
  },
  "dependencies": {
    "@cucumber/cucumber": "^10.3.1",
    "chai": "^4.3.10",
    "axios": "^1.6.2"
  },
  "devDependencies": {
    "@cucumber/cucumber": "^10.3.1",
    "@cucumber/pretty-formatter": "^1.0.0",
    "multiple-cucumber-html-reporter": "^3.6.0"
  }
}

// 2. cucumber.js - Configuration
module.exports = {
  default: {
    requireModule: ['@babel/register'],
    require: ['step-definitions/**/*.js', 'support/**/*.js'],
    format: [
      'progress-bar',
      'html:reports/cucumber-report.html',
      'json:reports/cucumber-report.json'
    ],
    formatOptions: {
      snippetInterface: 'async-await',
      snippetSyntax: 'typescript',
    },
    publishQuiet: true,
    dryRun: false,
    failFast: false,
    strict: true,
    worldParameters: {
      apiUrl: 'https://api.example.com',
      timeout: 10000
    }
  }
};

// 3. features/user-registration.feature - Gherkin Feature File
Feature: User Registration
  As a new user
  I want to register for an account
  So that I can access the application features

  Scenario: Successful user registration
    Given I am on the registration page
    When I enter valid registration details:
      | firstName | John    |
      | lastName  | Doe     |
      | email     | [email protected] |
      | password  | Secret123! |
    And I submit the registration form
    Then I should see a success message
    And I should receive a confirmation email
    And my account should be created in the system

  Scenario: Registration with invalid email
    Given I am on the registration page
    When I enter registration details with invalid email:
      | firstName | Jane    |
      | lastName  | Smith   |
      | email     | invalid-email |
      | password  | Secret123! |
    And I submit the registration form
    Then I should see an email validation error
    And my account should not be created

  @smoke
  Scenario Outline: Registration validation
    Given I am on the registration page
    When I enter registration details with "field" value "value"
    And I submit the registration form
    Then I should see "errorType" error message

    Examples:
      | field      | value          | errorType          |
      | email      | invalid-email  | email validation   |
      | password   | 123            | password strength  |
      | firstName  |                | required field     |
      | lastName   |                | required field     |

// 4. step-definitions/user-registration-steps.js - Step Definitions
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
const RegistrationPage = require('../pages/registration-page');
const UserApiClient = require('../support/api-client');

let registrationPage;
let apiClient;
let userData;

// Before and After hooks
Before(async function () {
  registrationPage = new RegistrationPage();
  apiClient = new UserApiClient(this.parameters.apiUrl);
  userData = {};
});

After(async function () {
  // Cleanup: Delete test users
  if (userData.email) {
    await apiClient.deleteUser(userData.email);
  }
});

Given('I am on the registration page', async function () {
  await registrationPage.navigateTo();
  expect(await registrationPage.isLoaded()).to.be.true;
});

When('I enter valid registration details:', async function (dataTable) {
  userData = dataTable.rowsHash();
  await registrationPage.fillForm(userData);
});

When('I enter registration details with invalid email:', async function (dataTable) {
  userData = dataTable.rowsHash();
  await registrationPage.fillForm(userData);
});

When('I enter registration details with {string} value {string}', async function (field, value) {
  userData[field] = value;
  await registrationPage.fillField(field, value);
});

When('I submit the registration form', async function () {
  await registrationPage.submitForm();
});

Then('I should see a success message', async function () {
  const successMessage = await registrationPage.getSuccessMessage();
  expect(successMessage).to.include('Registration successful');
});

Then('I should receive a confirmation email', async function () {
  // This would integrate with email service
  const emailReceived = await apiClient.checkConfirmationEmail(userData.email);
  expect(emailReceived).to.be.true;
});

Then('my account should be created in the system', async function () {
  const userExists = await apiClient.userExists(userData.email);
  expect(userExists).to.be.true;
});

Then('I should see an email validation error', async function () {
  const errorMessage = await registrationPage.getErrorMessage();
  expect(errorMessage).to.include('Invalid email format');
});

Then('my account should not be created', async function () {
  const userExists = await apiClient.userExists(userData.email);
  expect(userExists).to.be.false;
});

Then('I should see {string} error message', async function (errorType) {
  const errorMessage = await registrationPage.getErrorMessage();
  const errorMessages = {
    'email validation': 'Invalid email format',
    'password strength': 'Password must be at least 8 characters',
    'required field': 'This field is required'
  };

  expect(errorMessage).to.include(errorMessages[errorType]);
});

// 5. pages/registration-page.js - Page Object Model
class RegistrationPage {
  constructor() {
    this.url = '/register';
    this.firstNameInput = '#firstName';
    this.lastNameInput = '#lastName';
    this.emailInput = '#email';
    this.passwordInput = '#password';
    this.submitButton = '#registerButton';
    this.successMessage = '.success-message';
    this.errorMessage = '.error-message';
  }

  async navigateTo() {
    await page.goto(this.url);
  }

  async isLoaded() {
    await page.waitForSelector(this.firstNameInput);
    await page.waitForSelector(this.lastNameInput);
    await page.waitForSelector(this.emailInput);
    await page.waitForSelector(this.passwordInput);
    await page.waitForSelector(this.submitButton);
    return true;
  }

  async fillForm(userData) {
    await page.fill(this.firstNameInput, userData.firstName || '');
    await page.fill(this.lastNameInput, userData.lastName || '');
    await page.fill(this.emailInput, userData.email || '');
    await page.fill(this.passwordInput, userData.password || '');
  }

  async fillField(field, value) {
    const fieldSelectors = {
      firstName: this.firstNameInput,
      lastName: this.lastNameInput,
      email: this.emailInput,
      password: this.passwordInput
    };

    await page.fill(fieldSelectors[field], value);
  }

  async submitForm() {
    await page.click(this.submitButton);
  }

  async getSuccessMessage() {
    await page.waitForSelector(this.successMessage);
    return await page.textContent(this.successMessage);
  }

  async getErrorMessage() {
    await page.waitForSelector(this.errorMessage);
    return await page.textContent(this.errorMessage);
  }
}

module.exports = RegistrationPage;

// 6. support/api-client.js - API Support
const axios = require('axios');

class UserApiClient {
  constructor(baseUrl) {
    this.client = axios.create({
      baseURL: baseUrl,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    });
  }

  async userExists(email) {
    try {
      const response = await this.client.get(`/users?email=${email}`);
      return response.data.length > 0;
    } catch (error) {
      return false;
    }
  }

  async deleteUser(email) {
    try {
      await this.client.delete(`/users/${email}`);
      return true;
    } catch (error) {
      return false;
    }
  }

  async checkConfirmationEmail(email) {
    // Mock implementation - would integrate with email service
    return true;
  }
}

module.exports = UserApiClient;

// 7. support/hooks.js - Custom Hooks
const { Before, After, BeforeAll, AfterAll } = require('@cucumber/cucumber');
const { chromium } = require('playwright');

let browser;
let page;

BeforeAll(async function () {
  browser = await chromium.launch({ headless: true });
});

AfterAll(async function () {
  await browser.close();
});

Before(async function () {
  page = await browser.newPage();
  global.page = page;
});

After(async function () {
  await page.close();
});

// Tag-based hooks
Before({ tags: '@smoke' }, async function () {
  console.log('Running smoke test...');
});

Before({ tags: '@api' }, async function () {
  // Setup API test environment
});

After({ tags: '@cleanup' }, async function () {
  // Perform cleanup operations
});

💻 高级 Cucumber 模式和最佳实践 javascript

🟡 intermediate ⭐⭐⭐

复杂的 BDD 模式,包括数据表格、场景大纲、钩子、标签和企业级 Cucumber 实现

⏱️ 75 min 🏷️ cucumber, bdd, advanced, patterns
Prerequisites: Cucumber basics, BDD concepts, JavaScript advanced, Page Object Model
// Cucumber Advanced Patterns and Best Practices

// 1. features/e-commerce-shopping.feature - Complex Feature File
Feature: E-commerce Shopping Cart
  As a customer
  I want to add products to my cart and complete purchases
  So that I can buy products online

  Background:
    Given I am logged in as a customer
    And the following products exist in the catalog:
      | id  | name               | price  | category    | stock |
      | 1   | Laptop Pro         | 999.99 | Electronics | 10    |
      | 2   | Wireless Mouse     | 29.99  | Electronics | 50    |
      | 3   | Coffee Maker       | 79.99  | Home        | 25    |
      | 4   | Bluetooth Headphones | 149.99 | Electronics | 30 |

  @shopping @regression
  Scenario: Add single product to cart
    When I add product "Laptop Pro" to cart
    Then my cart should contain 1 item
    And the cart total should be $999.99
    And the product stock should be updated to 9

  @shopping @regression
  Scenario: Add multiple products to cart
    When I add the following products to cart:
      | product name         | quantity |
      | Wireless Mouse       | 2        |
      | Bluetooth Headphones | 1        |
      | Coffee Maker         | 1        |
    Then my cart should contain 4 items
    And the cart total should be $289.96
    And each product stock should be updated accordingly

  @checkout @critical
  Scenario Outline: Complete purchase with different payment methods
    Given I have the following products in my cart:
      | product name    | quantity |
      | Laptop Pro      | 1        |
    When I proceed to checkout
    And I select payment method
    And I enter payment details
    Then I should see order confirmation
    And I should receive order confirmation email
    And the order should be created in the system
    And my cart should be empty

    Examples:
      | payment method    |
      | Credit Card       |
      | PayPal            |
      | Bank Transfer     |

  @inventory @edge-case
  Scenario: Attempt to add out-of-stock product
    When the product "Laptop Pro" stock is 0
    And I attempt to add "Laptop Pro" to cart
    Then I should see "out of stock" error message
    And the product should not be added to my cart

  @discount @promotion
  Scenario: Apply discount code to cart
    Given I have the following products in my cart:
      | product name    | quantity |
      | Laptop Pro      | 1        |
      | Wireless Mouse  | 1        |
    When I apply discount code "SAVE10"
    Then a 10% discount should be applied
    And the cart total should be $923.98

// 2. step-definitions/shopping-cart-steps.js - Advanced Step Definitions
const { Given, When, Then, defineParameterType } = require('@cucumber/cucumber');
const { expect } = require('chai');
const ShoppingCart = require('../support/shopping-cart');
const ProductCatalog = require('../support/product-catalog');
const PaymentProcessor = require('../support/payment-processor');

// Custom parameter type for product identification
defineParameterType({
  name: 'product',
  regexp: /"([^"]+)"/,
  transformer(name) {
    return ProductCatalog.findByName(name);
  }
});

// Custom parameter type for currency amounts
defineParameterType({
  name: 'amount',
  regexp: /$([0-9.]+)/,
  transformer(amount) {
    return parseFloat(amount);
  }
});

let shoppingCart;
let productCatalog;
let paymentProcessor;
let currentUser;

Before(async function () {
  shoppingCart = new ShoppingCart();
  productCatalog = new ProductCatalog();
  paymentProcessor = new PaymentProcessor();
  currentUser = null;
});

Given('I am logged in as a customer', async function () {
  currentUser = await this.createUser({
    email: '[email protected]',
    role: 'customer'
  });
});

Given('the following products exist in the catalog:', async function (dataTable) {
  for (const row of dataTable.hashes()) {
    await productCatalog.addProduct({
      id: parseInt(row.id),
      name: row.name,
      price: parseFloat(row.price),
      category: row.category,
      stock: parseInt(row.stock)
    });
  }
});

When('I add product {product} to cart', async function (product) {
  await shoppingCart.addItem({
    productId: product.id,
    quantity: 1,
    userId: currentUser.id
  });
});

When('I add the following products to cart:', async function (dataTable) {
  for (const row of dataTable.hashes()) {
    const product = await productCatalog.findByName(row['product name']);
    await shoppingCart.addItem({
      productId: product.id,
      quantity: parseInt(row.quantity),
      userId: currentUser.id
    });
  }
});

Then('my cart should contain {int} item(s)', async function (expectedCount) {
  const cart = await shoppingCart.getCart(currentUser.id);
  expect(cart.items.length).to.equal(expectedCount);
});

Then('the cart total should be {amount}', async function (expectedTotal) {
  const cart = await shoppingCart.getCart(currentUser.id);
  expect(cart.total).to.equal(expectedTotal);
});

Then('the product stock should be updated to {int}', async function (expectedStock) {
  const product = await productCatalog.findByName('Laptop Pro');
  expect(product.stock).to.equal(expectedStock);
});

Then('each product stock should be updated accordingly', async function () {
  const cart = await shoppingCart.getCart(currentUser.id);
  for (const item of cart.items) {
    const product = await productCatalog.findById(item.productId);
    const originalStock = 10; // From background setup
    expect(product.stock).to.equal(originalStock - item.quantity);
  }
});

// 3. support/data-tables.js - Data Table Utilities
class DataTableHelper {
  static toMap(dataTable, keyColumn, valueColumn) {
    const result = {};
    for (const row of dataTable.hashes()) {
      result[row[keyColumn]] = row[valueColumn];
    }
    return result;
  }

  static toArrayOfMaps(dataTable) {
    return dataTable.rows().map(row => {
      const headers = dataTable.raw()[0];
      return headers.reduce((obj, header, index) => {
        obj[header] = row[index];
        return obj;
      }, {});
    });
  }

  static toList(dataTable, column) {
    return dataTable.rows().map(row => row[column]);
  }

  static validateRequiredColumns(dataTable, requiredColumns) {
    const headers = dataTable.raw()[0];
    for (const column of requiredColumns) {
      if (!headers.includes(column)) {
        throw new Error(`Required column '${column}' not found in data table`);
      }
    }
  }
}

module.exports = DataTableHelper;

// 4. support/world-parameters.js - Custom World
const { setWorldConstructor } = require('@cucumber/cucumber');

class CustomWorld {
  constructor({ attach, parameters }) {
    this.attach = attach;
    this.parameters = parameters;
    this.testData = new Map();
    this.apiClient = null;
    this.database = null;
  }

  // Test data management
  setTestData(key, value) {
    this.testData.set(key, value);
  }

  getTestData(key) {
    return this.testData.get(key);
  }

  // API client management
  async createApiClient(options = {}) {
    this.apiClient = new ApiClient({
      baseURL: this.parameters.apiUrl,
      ...options
    });
    return this.apiClient;
  }

  // Database helpers
  async createDatabaseConnection() {
    this.database = new DatabaseClient(this.parameters.databaseUrl);
    await this.database.connect();
    return this.database;
  }

  // Screenshot attachment
  async attachScreenshot(name) {
    if (this.page) {
      const screenshot = await this.page.screenshot({
        encoding: 'base64',
        fullPage: true
      });
      await this.attach(screenshot, `image/png`);
      await this.attach(`Screenshot: ${name}`, 'text/plain');
    }
  }

  // Response attachment
  attachApiResponse(response) {
    const responseBody = JSON.stringify(response.data, null, 2);
    this.attach(`API Response (${response.status}):`, 'text/plain');
    this.attach(responseBody, 'application/json');
  }
}

setWorldConstructor(CustomWorld);

// 5. support/hooks.js - Advanced Hook Management
const { Before, After, BeforeAll, AfterAll } = require('@cucumber/cucumber');
const DatabaseClient = require('./database-client');

// Global hooks
BeforeAll(async function () {
  console.log('Setting up test environment...');
  await setupTestEnvironment();
});

AfterAll(async function () {
  console.log('Cleaning up test environment...');
  await cleanupTestEnvironment();
});

// Tag-based hooks
Before({ tags: '@api' }, async function (world) {
  await world.createApiClient({
    timeout: 30000,
    retries: 3
  });
});

Before({ tags: '@database' }, async function (world) {
  await world.createDatabaseConnection();
});

After({ tags: '@database' }, async function (world) {
  if (world.database) {
    await world.database.close();
  }
});

// Conditional hooks based on scenario name
Before(async function (scenario) {
  if (scenario.pickle.name.includes('performance')) {
    this.testType = 'performance';
    this.startTime = Date.now();
  }
});

After(async function (scenario) {
  // Attach screenshot if scenario failed
  if (scenario.result.status === 'FAILED') {
    await this.attachScreenshot(`failed-${scenario.pickle.name}`);
  }

  // Log performance metrics
  if (this.testType === 'performance') {
    const duration = Date.now() - this.startTime;
    console.log(`Scenario '${scenario.pickle.name}' completed in ${duration}ms`);
  }
});

// 6. support/reporting.js - Custom Reporting
const { Before, After } = require('@cucumber/cucumber');

Before(async function (scenario) {
  this.scenarioData = {
    name: scenario.pickle.name,
    tags: scenario.pickle.tags.map(tag => tag.name),
    startTime: new Date()
  };
});

After(async function (scenario) {
  const endTime = new Date();
  const duration = endTime - this.scenarioData.startTime;

  // Generate custom report data
  const reportData = {
    scenario: this.scenarioData.name,
    status: scenario.result.status,
    duration: duration,
    tags: this.scenarioData.tags,
    timestamp: endTime.toISOString()
  };

  // Store for custom reporting
  if (!global.customReports) {
    global.customReports = [];
  }
  global.customReports.push(reportData);
});

// 7. support/custom-matchers.js - Custom Chai Matchers
const { expect } = require('chai');

// Custom matcher for price validation
expect.addMethod('toCost', function (expectedAmount) {
  const actual = this._obj;
  const tolerance = 0.01; // Allow for floating point precision

  this.assert(
    Math.abs(actual - expectedAmount) <= tolerance,
    `expected #{this} to cost ${expectedAmount}, but it costs ${actual}`,
    `expected #{this} to not cost ${expectedAmount}, but it does`
  );
});

// Custom matcher for product stock validation
expect.addMethod('toHaveStock', function (expectedStock) {
  const product = this._obj;
  this.assert(
    product.stock === expectedStock,
    `expected product #{product.name} to have stock ${expectedStock}, but has ${product.stock}`,
    `expected product #{product.name} to not have stock ${expectedStock}, but it does`
  );
});

// Usage examples:
// expect(cart.total).toCost(99.99);
// expect(product).toHaveStock(10);