deno.com
本页内容

行为驱动开发 (BDD)

行为驱动开发 (BDD) 是一种软件开发方法,它鼓励开发人员、质量保证人员和非技术利益相关者之间的协作。BDD 专注于通过以自然、通用的语言编写的示例来定义应用程序的行为,所有利益相关者都能理解这些示例。

Deno 的标准库提供了一个 BDD 风格的测试模块,它允许你以一种既便于非技术利益相关者阅读又便于实现的方式来组织测试。在本教程中,我们将探讨如何使用 BDD 模块为你的应用程序创建描述性的测试套件。

BDD 简介 Jump to heading

BDD 通过使用易于阅读的自然语言编写测试来扩展测试驱动开发 (TDD)。BDD 鼓励我们不是考虑“测试”,而是考虑“规范”或“规格”,它们从用户的角度描述软件应该如何行为。这种方法有助于使测试专注于代码应该做什么,而不是如何实现。

BDD 的基本要素包括

  • Describe 块,用于对相关规范进行分组
  • It 语句,用于表达单一行为
  • Before/After 钩子,用于设置和清理操作

使用 Deno 的 BDD 模块 Jump to heading

要在 Deno 中开始 BDD 测试,我们将使用 Deno 标准库中的 @std/testing/bdd 模块。

首先,让我们导入必要的函数

import {
  afterAll,
  afterEach,
  beforeAll,
  beforeEach,
  describe,
  it,
} from "jsr:@std/testing/bdd";
import { assertEquals, assertThrows } from "jsr:@std/assert";

这些导入提供了核心的 BDD 函数

  • describe 创建一个块来分组相关的测试
  • it 声明一个测试用例,用于验证特定行为
  • beforeEach/afterEach 在每个测试用例运行之前或之后运行
  • beforeAll/afterAll 在一个 describe 块中的所有测试运行之前或之后运行一次

我们还将使用 @std/assert 中的断言函数来验证我们的预期。

编写你的第一个 BDD 测试 Jump to heading

让我们创建一个简单的计算器模块并使用 BDD 对其进行测试

calculator.ts
export class Calculator {
  private value: number = 0;

  constructor(initialValue: number = 0) {
    this.value = initialValue;
  }

  add(number: number): Calculator {
    this.value += number;
    return this;
  }

  subtract(number: number): Calculator {
    this.value -= number;
    return this;
  }

  multiply(number: number): Calculator {
    this.value *= number;
    return this;
  }

  divide(number: number): Calculator {
    if (number === 0) {
      throw new Error("Cannot divide by zero");
    }
    this.value /= number;
    return this;
  }

  get result(): number {
    return this.value;
  }
}

现在,让我们使用 BDD 风格来测试这个计算器

calculator_test.ts
import { afterEach, beforeEach, describe, it } from "jsr:@std/testing/bdd";
import { assertEquals, assertThrows } from "jsr:@std/assert";
import { Calculator } from "./calculator.ts";

describe("Calculator", () => {
  let calculator: Calculator;

  // Before each test, create a new Calculator instance
  beforeEach(() => {
    calculator = new Calculator();
  });

  it("should initialize with zero", () => {
    assertEquals(calculator.result, 0);
  });

  it("should initialize with a provided value", () => {
    const initializedCalculator = new Calculator(10);
    assertEquals(initializedCalculator.result, 10);
  });

  describe("add method", () => {
    it("should add a positive number correctly", () => {
      calculator.add(5);
      assertEquals(calculator.result, 5);
    });

    it("should handle negative numbers", () => {
      calculator.add(-5);
      assertEquals(calculator.result, -5);
    });

    it("should be chainable", () => {
      calculator.add(5).add(10);
      assertEquals(calculator.result, 15);
    });
  });

  describe("subtract method", () => {
    it("should subtract a number correctly", () => {
      calculator.subtract(5);
      assertEquals(calculator.result, -5);
    });

    it("should be chainable", () => {
      calculator.subtract(5).subtract(10);
      assertEquals(calculator.result, -15);
    });
  });

  describe("multiply method", () => {
    beforeEach(() => {
      // For multiplication tests, start with value 10
      calculator = new Calculator(10);
    });

    it("should multiply by a number correctly", () => {
      calculator.multiply(5);
      assertEquals(calculator.result, 50);
    });

    it("should be chainable", () => {
      calculator.multiply(2).multiply(3);
      assertEquals(calculator.result, 60);
    });
  });

  describe("divide method", () => {
    beforeEach(() => {
      // For division tests, start with value 10
      calculator = new Calculator(10);
    });

    it("should divide by a number correctly", () => {
      calculator.divide(2);
      assertEquals(calculator.result, 5);
    });

    it("should throw when dividing by zero", () => {
      assertThrows(
        () => calculator.divide(0),
        Error,
        "Cannot divide by zero",
      );
    });
  });
});

要运行此测试,请使用 deno test 命令

deno test calculator_test.ts

你将看到类似这样的输出

running 1 test from file:///path/to/calculator_test.ts
Calculator
  ✓ should initialize with zero 
  ✓ should initialize with a provided value 
  add method
    ✓ should add a positive number correctly 
    ✓ should handle negative numbers 
    ✓ should be chainable 
  subtract method
    ✓ should subtract a number correctly 
    ✓ should be chainable 
  multiply method
    ✓ should multiply by a number correctly 
    ✓ should be chainable 
  divide method
    ✓ should divide by a number correctly 
    ✓ should throw when dividing by zero 

ok | 11 passed | 0 failed (234ms)

使用嵌套的 describe 块组织测试 Jump to heading

BDD 的强大功能之一是能够嵌套 describe 块,这有助于分层组织测试。在计算器示例中,我们将每个方法的测试分组到其自己的 describe 块中。这不仅使测试更具可读性,而且在测试失败时更容易定位问题。

你可以嵌套 describe 块,但要注意不要嵌套过深,因为过度的嵌套会使测试难以理解。

钩子 Jump to heading

BDD 模块提供了四个钩子

  • beforeEach 在当前 describe 块中的每个测试之前运行
  • afterEach 在当前 describe 块中的每个测试之后运行
  • beforeAll 在当前 describe 块中的所有测试之前运行一次
  • afterAll 在当前 describe 块中的所有测试之后运行一次

beforeEach/afterEach Jump to heading

这些钩子非常适合

  • 为每个测试设置一个全新的测试环境
  • 在每个测试后清理资源
  • 确保测试隔离性

在计算器示例中,我们使用 beforeEach 在每个测试之前创建一个新的计算器实例,确保每个测试都从一个干净的状态开始。

beforeAll/afterAll Jump to heading

这些钩子对于以下情况很有用

  • 可以在测试之间共享的开销较大的设置操作
  • 设置和拆除数据库连接
  • 创建和清理共享资源

以下是你如何使用 beforeAllafterAll 的示例

describe("Database operations", () => {
  let db: Database;

  beforeAll(async () => {
    // Connect to the database once before all tests
    db = await Database.connect(TEST_CONNECTION_STRING);
    await db.migrate();
  });

  afterAll(async () => {
    // Disconnect after all tests are complete
    await db.close();
  });

  it("should insert a record", async () => {
    const result = await db.insert({ name: "Test" });
    assertEquals(result.success, true);
  });

  it("should retrieve a record", async () => {
    const record = await db.findById(1);
    assertEquals(record.name, "Test");
  });
});

Gherkin 与 JavaScript 风格的 BDD Jump to heading

如果你熟悉 Cucumber 或其他 BDD 框架,你可能期望使用“Given-When-Then”语句的 Gherkin 语法。

Deno 的 BDD 模块使用的是 JavaScript 风格的语法,而不是 Gherkin。这种方法类似于 Mocha 或 Jasmine 等其他 JavaScript 测试框架。但是,你仍然可以通过以下方式遵循 BDD 原则

  1. 编写清晰、以行为为焦点的测试描述
  2. 组织你的测试以反映用户故事
  3. 在测试实现中遵循“安排-执行-断言”(Arrange-Act-Assert)模式

例如,你可以将 it 块组织成镜像 Given-When-Then 格式

describe("Calculator", () => {
  it("should add numbers correctly", () => {
    // Given
    const calculator = new Calculator();

    // When
    calculator.add(5);

    // Then
    assertEquals(calculator.result, 5);
  });
});

如果你需要带有自然语言规范的完整 Gherkin 支持,请考虑使用与 Deno 集成的专用 BDD 框架,例如 cucumber-js

使用 Deno 进行 BDD 的最佳实践 Jump to heading

编写易于人类阅读的测试 Jump to heading

BDD 测试应该像文档一样阅读。在你的 describeit 语句中使用清晰、描述性的语言

// Good
describe("User authentication", () => {
  it("should reject login with incorrect password", () => {
    // Test code
  });
});

// Not good
describe("auth", () => {
  it("bad pw fails", () => {
    // Test code
  });
});

保持测试的专注性 Jump to heading

每个测试都应该验证单一行为。避免在一个 it 块中测试多种行为

// Good
it("should add an item to the cart", () => {
  // Test adding to cart
});

it("should calculate the correct total", () => {
  // Test total calculation
});

// Bad
it("should add an item and calculate total", () => {
  // Test adding to cart
  // Test total calculation
});

使用特定上下文的设置 Jump to heading

当 describe 块中的测试需要不同的设置时,使用带有各自 beforeEach 钩子的嵌套 describe 块,而不是条件逻辑

// Good
describe("User operations", () => {
  describe("when user is logged in", () => {
    beforeEach(() => {
      // Setup logged-in user
    });

    it("should show the dashboard", () => {
      // Test
    });
  });

  describe("when user is logged out", () => {
    beforeEach(() => {
      // Setup logged-out state
    });

    it("should redirect to login", () => {
      // Test
    });
  });
});

// Avoid
describe("User operations", () => {
  beforeEach(() => {
    // Setup base state
    if (isLoggedInTest) {
      // Setup logged-in state
    } else {
      // Setup logged-out state
    }
  });

  it("should show dashboard when logged in", () => {
    isLoggedInTest = true;
    // Test
  });

  it("should redirect to login when logged out", () => {
    isLoggedInTest = false;
    // Test
  });
});

正确处理异步测试 Jump to heading

在测试异步代码时,请记住

  • 将你的测试函数标记为 async
  • 对 Promise 使用 await
  • 正确处理错误
it("should fetch user data asynchronously", async () => {
  const user = await fetchUser(1);
  assertEquals(user.name, "John Doe");
});

🦕 通过遵循本教程中概述的 BDD 原则和实践,你可以构建更可靠的软件,并巩固你对代码“业务逻辑”的理解。

请记住,BDD 不仅仅是语法或工具,它更是一种协作的方法来定义和验证应用程序的行为。最成功的 BDD 实施将这些技术实践与开发人员、测试人员、产品和业务利益相关者之间的定期交流结合起来。

要继续学习 Deno 中的测试,请探索标准库测试套件中的其他模块,例如模拟快照测试

您找到所需内容了吗?

隐私政策