autorenew

测试驱动开发 | Test-Driven Development, TDD

时分驱动 ( 测试驱动开发)是一种在生产代码之前优先编写测试的开发方法。该过程包括编写测试、创建通过测试的最少代码,然后改进代码。

关于测试驱动开发的问题?

基础知识和重要性

什么是测试驱动开发 (TDD)?

测试驱动开发 (TDD) 是一种软件开发方法,其中测试是在要验证的生产代码之前编写的。这是一个循环过程,开发人员编写一个测试来定义所需的改进或新功能,然后生成最少量的代码来通过该测试,最后将新代码重构为可接受的标准。 这是 TypeScript 中的一个基本示例:

  // Step 1: Write a failing test
  describe('Calculator', () => {
    it('adds two numbers', () => {
      const calculator = new Calculator();
      expect(calculator.add(2, 3)).toEqual(5);
    });
  });
  // Step 2: Write just enough code to make the test pass
  class Calculator {
    add(a: number, b: number): number {
      return a + b;
    }
  }
  // Step 3: Refactor if necessary
  // In this simple case, no refactoring is needed.

在TDD中,模拟对象通常用于模拟复杂依赖关系的行为,使测试能够专注于正在开发的代码,而不受外部系统的干扰。 TDD 强调测试优先开发,鼓励简单的设计并激发信心。它与各种软件开发方法兼容,例如敏捷,并且可以通过从新功能开始或通过测试逐步覆盖遗留代码来集成到现有项目中。 虽然 TDD 可以带来更高质量的软件,但它需要纪律和对其原理的理解,以避免常见的陷阱,例如编写过于广泛的测试或没有充分重构代码。 JUnit for Java、Mocha for JavaScript 和 xUnit for.NET 等工具和框架通过提供编写和运行测试的结构化方法来促进 TDD。

为什么 TDD 在软件开发中很重要?

TDD 在软件开发中非常重要,因为它确保编码、测试和设计同时进行,从而提高开发人员生产力代码质量。通过专注于小的增量更改,开发人员可以避免范围蔓延并确保在继续之前对每个功能进行正确的测试。 TDD 鼓励简单的设计激发对软件的信心,因为添加新功能不会破坏现有功能。这种信心允许积极的重构,从而保持代码库的干净和可维护。此外,TDD 创建了一套全面的单元测试,可以随时运行以检测回归。它还有助于更好的文档,因为测试可以作为系统行为的规范。 TDD 对可测试性的强调也导致了更多模块化和灵活的代码,使其更容易适应变化。在团队环境中,TDD 有助于最小化集成期间引入的bugs,并提供安全网,使多个开发人员能够在同一代码库上工作,同时降低冲突或回归的风险。最后,TDD 非常适合敏捷和迭代开发实践,符合持续改进和适应的精神。

TDD 的关键原则是什么?

TDD 的关键原则是:

  • 首先编写测试:在编写功能代码之前,为新功能创建特定测试。该测试最初应该会失败,因为该功能尚未实现。
  • 小步骤:以小增量工作,一次编写一个测试和相应的代码。这有助于专注于功能的一方面并降低复杂性。
  • 失败测试:新测试的第一次运行应该会导致失败,验证测试是否正确检测到新功能的缺失。
  • 快速反馈:应经常运行测试以提供有关更改的即时反馈。这有助于在开发周期的早期识别和解决问题。
  • 自信地重构:测试通过后,重构代码以提高其结构和可读性。现有的测试提供了一个安全网,确保功能保持完整。
  • 持续集成:经常将代码集成到主分支中并运行测试以尽早发现集成问题。
  • 清晰易懂的测试:测试应该清晰地编写并作为代码的文档。它们应该易于阅读和理解。
  • 每个测试一个逻辑断言:每个测试都应该验证代码的一个方面,以保持测试的重点和可理解性。
  • 避免测试内部:关注行为而不是内部实现。测试不应因不影响行为的代码结构更改而中断。
  • 保持测试快速:测试需要快速运行,以免减慢开发过程。缓慢的测试可能会成为瓶颈,并阻碍开发人员频繁运行测试套件

TDD 如何提高软件质量?

TDD 通过确保 测试覆盖率 较高并且在编写代码时考虑到 可测试性 来改进 软件质量。通过在实际代码之前编写测试,开发人员被迫从一开始就考虑边缘情况和潜在的bugs,从而产生更健壮和可靠的代码。这种方法还促进更简单、更模块化的设计,因为难以测试的代码通常表明结构较差。 此外,TDD 的红-绿-重构周期鼓励持续重构,这有助于维护干净的代码库并减少技术债务。由于测试是首先编写的,因此开发人员拥有一个安全网,可以让他们充满信心地进行重构,因为他们知道任何引入的回归都会立即被捕获。 TDD 的迭代性质导致了详细的回归套件随着代码库的增长而增长,提供有关更改影响的即时反馈。该套件成为维持长期质量的宝贵资产,因为它可以在开发周期的早期检测到问题,从而减少后期修复bugs 的成本和工作量。 TDD 还通过充当系统行为的实时规范的测试来促进更好的文档。这可以提高当前和未来开发人员对代码的理解和可维护性。 总之,TDD 通过培育一个优先考虑测试的开发环境来增强软件质量,从而产生更干净、更可维护的代码,并降低进入生产时出现缺陷的可能性。

传统测试和 TDD 有什么区别?

传统测试通常发生在开发阶段之后,测试人员编写并执行测试以验证已编写代码的功能。这种方法通常会导致最后测试周期,其中测试是一个单独的阶段,并且可能导致在开发过程的后期发现bugs。 相比之下,测试驱动开发 (TDD) 是一种测试优先方法,其中测试实际代码之前编写。开发人员首先编写一个失败的测试,定义所需的改进或新功能,然后生成通过该测试的最少量的代码,最后将新代码重构为可接受的标准。 主要区别是:

  • 时机:传统测试是在编码之后完成,而 TDD 要求在编码之前编写测试。
  • 测试的作用:在传统测试中,测试充当验证工具;在 TDD 中,它们指导设计和开发。
  • 反馈循环:TDD 提供快速反馈循环,尽早发现问题,而传统测试可能会在周期后期发现问题。
  • 设计影响:TDD 影响设计更加模块化和可测试,而传统测试则适应现有设计。
  • Bug 预防与检测:TDD 侧重于通过测试优先的开发来防止错误,而传统测试侧重于在实施后检测错误。 TDD 对测试优先开发的强调从根本上改变了测试在软件开发生命周期中的作用,将它们集成到软件的设计和构建中,而不是将它们视为一个单独的阶段。

TDD 流程

TDD 流程涉及哪些步骤?

TDD 流程包括以下步骤:

  1. 确定需求或需要实现的功能。

  2. **写一个测试用例**失败是因为该功能尚未实现。这是“红色”阶段,测试将失败,表明新功能不存在。

    it('should add two numbers', () => {
      expect(add(1, 2)).toEqual(3);
    });
  1. 编写最少的代码需要通过测试。这是“绿色”阶段,您专注于让测试尽快通过,而不用担心代码质量。
    function add(a, b) {
      return a + b;
    }
  1. 重构代码在不改变其行为的情况下改进其结构、可读性或性能。这是“重构”阶段,您清理代码,同时确保所有测试仍然通过。
    function add(a, b) {
      // Refactored implementation, if necessary
      return a + b;
    }
  1. 重复下一个功能或需求的周期。 在整个过程中,每次更改后都运行所有测试,以确保不会引入回归。这种测试-代码-重构的迭代循环有助于构建健壮且经过良好测试的代码库。

  2. 确定需求或需要实现的功能。

  3. **写一个测试用例**失败是因为该功能尚未实现。这是“红色”阶段,测试将失败,表明新功能不存在。

    it('should add two numbers', () => {
      expect(add(1, 2)).toEqual(3);
    });
  1. 编写最少的代码需要通过测试。这是“绿色”阶段,您专注于让测试尽快通过,而不用担心代码质量。
    function add(a, b) {
      return a + b;
    }
  1. 重构代码在不改变其行为的情况下改进其结构、可读性或性能。这是“重构”阶段,您清理代码,同时确保所有测试仍然通过。
    function add(a, b) {
      // Refactored implementation, if necessary
      return a + b;
    }
  1. 重复下一个功能或需求的周期。

TDD 中的红-绿-重构周期是什么?

红-绿-重构周期是 TDD 的基本节奏,它促进了规范的开发方法:

  1. 红色:编写一个描述预期行为或功能的新测试。运行 测试套件 查看此测试失败(红色),确认该功能不存在或尚未满足该行为。
    it('should add two numbers', () => {
      expect(add(1, 2)).toEqual(3);
    });
  1. 绿色:实现最简单的代码以使失败的测试通过(绿色)。这里的重点是功能,而不是完美。
    function add(a, b) {
      return a + b;
    }
  1. 重构:清理新代码,确保其与现有代码库良好契合。此步骤涉及删除重复、提高可读性以及在不更改行为的情况下应用设计模式。
    // Refactor if necessary, but the above function is already simple.

对每个新功能或行为增量地重复此循环,确保测试始终在重构阶段后通过。此过程有助于维护干净的代码库,并提供有关更改影响的即时反馈。

  1. 红色:编写一个描述预期行为或功能的新测试。运行 测试套件 查看此测试失败(红色),确认该功能不存在或尚未满足该行为。
    it('should add two numbers', () => {
      expect(add(1, 2)).toEqual(3);
    });
  1. 绿色:实现最简单的代码以使失败的测试通过(绿色)。这里的重点是功能,而不是完美。
    function add(a, b) {
      return a + b;
    }
  1. 重构:清理新代码,确保其与现有代码库良好契合。此步骤涉及删除重复、提高可读性以及在不更改行为的情况下应用设计模式。
    // Refactor if necessary, but the above function is already simple.

如何在 TDD 中编写失败的测试?

在 TDD 中编写失败的测试涉及以下步骤:

  1. 确定您的应用程序需要实现的特定要求或一项功能。
  2. 编写 测试用例 断言该功能的预期行为。该测试最初应该设计为失败,因为该功能尚未实现。
  3. 对测试函数使用描述性命名,以清楚地说明其测试的内容。
  4. 在测试主体中,设置任何必要的 测试数据 或模拟依赖项。
  5. 使用测试数据调用您想要实现的方法或函数。
  6. 断言预期结果。这可能是检查返回值、状态更改或与模拟的交互。 以下是使用 Jest 的 TypeScript 示例:
  test('should add two numbers', () => {
    // Arrange
    const calculator = new Calculator();
    // Act
    const result = calculator.add(1, 2);
    // Assert
    expect(result).toBe(3);
  });

在此示例中,Calculator 类及其add 方法尚未实现。运行此测试将导致失败,这是红-绿-重构循环的红色阶段的预期结果。失败的测试到位后,您将编写最少量的代码以使测试通过,进入绿色阶段。

如何在 TDD 中让失败的测试通过?

要在 TDD 中通过失败的测试,请执行以下步骤:

  1. 分析失败的测试,以了解当前实现未满足的预期行为。
  2. 编写最简单的代码,可以使测试通过。该代码不需要是完美的或最终的;它只需要满足测试的断言。
  3. **运行测试套件**以确保新代码使之前失败的测试通过,而不会导致任何其他测试失败。
  4. 重构代码以提高清晰度、性能和可维护性,同时确保所有测试继续通过。这可能涉及清理您刚刚编写的代码以使测试通过或改进受更改影响的代码库的其他部分。
  5. 重复每个新测试的循环,逐步构建和改进代码库。 这是 TypeScript 中的一个简单示例:
  // Initial failing test for a function that adds two numbers
  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
  });
  // Implementation that makes the test pass
  function add(a: number, b: number): number {
    return a + b; // Simplest implementation to pass the test
  }

请记住,目标是编写足以通过测试的代码,而不是预测未来的需求或添加额外的功能。这可以保持开发的重点并避免过度设计。

  1. 分析失败的测试,以了解当前实现未满足的预期行为。
  2. 编写最简单的代码,可以使测试通过。该代码不需要是完美的或最终的;它只需要满足测试的断言。
  3. 运行测试套件 以确保新代码使之前失败的测试通过,而不会导致任何其他测试失败。
  4. 重构代码以提高清晰度、性能和可维护性,同时确保所有测试继续通过。这可能涉及清理您刚刚编写的代码以使测试通过或改进受更改影响的代码库的其他部分。
  5. 重复每个新测试的循环,逐步构建和改进代码库。

TDD 中的重构意味着什么?

TDD 中的重构是在不改变现有代码外部行为的情况下改进其内部结构的过程。这是测试通过后(绿色阶段)红-绿-重构循环中的关键步骤。目标是增强代码可读性、降低复杂性并改进可维护性,同时确保系统功能保持完整。 在重构期间,您可能:

  • 简化代码通过分解复杂的方法。

  • 删除重复坚持 DRY(不要重复自己)原则。

  • 改进名字让变量、方法和类更好地反映其用途。

  • 重新组织代码完善其逻辑结构。

  • 优化性能通过改变算法而不影响结果。 重构由现有测试的安全网支持,这些测试在更改后必须继续通过。这确保重构不会引入新的bugs。这是一个迭代过程,可以逐步改进代码库,使其更容易随着时间的推移进行扩展和维护。 这是 TypeScript 中的一个简单示例:

  // Before refactoring
  function calculateArea(diameter) {
    return Math.PI * (diameter / 2) * (diameter / 2);
  }
  // After refactoring
  function calculateRadius(diameter) {
    return diameter / 2;
  }
  function calculateArea(diameter) {
    const radius = calculateRadius(diameter);
    return Math.PI * radius * radius;
  }

在此示例中,calculateArea 函数被重构为使用新的 calculateRadius 函数,从而提高了 calculateRadius 逻辑的可读性和潜在的可重用性。

  • 简化代码通过分解复杂的方法。

  • 删除重复坚持 DRY(不要重复自己)原则。

  • 改进名字让变量、方法和类更好地反映其用途。

  • 重新组织代码完善其逻辑结构。

  • 优化性能通过改变算法而不影响结果。

TDD 实践

实施 TDD 的最佳实践有哪些?

实施 TDD 的最佳实践:

  • 从小处开始:从简单的测试开始,然后再进行更复杂的场景。这有助于理解流程并保持专注于一次解决一个问题。
  • 每次测试测试一个概念:确保每个 测试用例 都专注于单一行为或功能,以简化调试并提供明确的意图。
  • 保持测试快速:优化测试执行时间以鼓励频繁的测试运行,这对于即时反馈至关重要。
  • 使用描述性测试名称:清楚地命名测试以传达其目的和预期结果,有助于 可维护性 和可读性。
  • 充满信心地重构:在实现绿色后,重构代码,同时保持测试通过,以在不改变行为的情况下提高代码质量。
  • 隔离测试:避免测试之间的依赖关系,以确保它们可以独立且以任何顺序运行。
  • 测试接口,而不是实现:关注预期行为而不是内部工作,以避免重构时的脆弱测试。
  • 使用版本控制:在每个通过的测试周期后提交,以记录开发过程并在必要时方便回滚。
  • 结对编程:与其他开发人员合作以获得不同的观点并增强测试覆盖率
  • 持续集成 (CI):将 TDD 与 CI 系统集成,在每次提交时自动运行测试,确保立即检测到集成问题。
  • 保持纪律:严格遵守红绿重构循环,以保持 TDD 流程的完整性。
  • 审查和调整:定期评估测试和 TDD 方法的有效性,并愿意调整策略以改善结果。

TDD 如何集成到现有项目中?

将 TDD 集成到现有项目中需要采取战略方法。首先选择应用程序的一个小的、可管理的部分来应用 TDD,例如需要重构的新功能或模块。这使得团队能够适应 TDD 工作流程,而不会让他们感到不知所措。 对团队进行 TDD 实践培训(如果他们还不熟悉)。确保每个人都了解首先编写测试和红-绿-重构周期的重要性。鼓励结对编程,在团队内传播 TDD 知识和实践。 为 TDD 工作设置专用分支,以避免破坏主代码库。这样可以进行实验和学习,而不会影响正在进行的开发。 持续集成 通过定期将 TDD 分支合并回主代码库。这有助于及早发现集成问题,并降低与主要开发工作偏离太远的风险。 逐步重构遗留代码。当您需要添加功能或修复现有代码中的 bug 时,请先为该特定部分编写测试,然后继续进行更改。随着时间的推移,这将增加遗留代码的测试覆盖率使用 CI/CD 工具自动构建和测试过程。这可确保测试自动且频繁地运行,从而提供有关代码运行状况的即时反馈。 监控并调整流程。通过回顾来讨论什么是有效的、什么是无效的,并相应地调整方法。持续改进是将 TDD 成功集成到现有项目的关键。

TDD 中有哪些常见陷阱以及如何避免它们?

TDD 中的常见陷阱包括:

  • 过度依赖单元测试:虽然单元测试至关重要,但它们无法发现集成问题。平衡 TDD 与更高级别的测试,以确保系统范围内的功能。

  • 重构不充分:跳过重构步骤可能会导致代码债务和维护问题。始终分配时间进行重构以保持代码质量。

  • 预先编写太多测试:这可能会导致难以重构的僵化代码。编写足够的测试来驱动下一个功能的开发。

  • 测试内部实现:关注行为而不是内部结构,以避免因代码结构的任何变化而破坏的脆弱测试。

  • 不测试边缘情况:确保测试涵盖广泛的输入,包括边缘情况,以防止在不太常见的情况下使用bugs

  • 忽略测试可维护性:测试应该像生产代码一样干净且可维护。使用描述性名称和结构测试以便于理解和修改。

  • 缺乏持续集成:将 TDD 与 CI/CD 管道集成以尽早发现问题并确保频繁运行测试。 通过以下方式避免这些陷阱:

  • 平衡不同级别的测试(单元、集成、系统)。

  • 定期重构并以与生产代码相同的方式对待测试代码。

  • 增量编写测试并关注代码的行为。

  • 经常运行测试并将其集成到您的 CI/CD 工作流程中。

  • 审查和维护测试以保持其有效性和相关性。

  • 过度依赖单元测试:虽然单元测试至关重要,但它们无法发现集成问题。平衡 TDD 与更高级别的测试,以确保系统范围内的功能。

  • 重构不充分:跳过重构步骤可能会导致代码债务和维护问题。始终分配时间进行重构以保持代码质量。

  • 预先编写太多测试:这可能会导致难以重构的僵化代码。编写足够的测试来驱动下一个功能的开发。

  • 测试内部实现:关注行为而不是内部结构,以避免因代码结构的任何变化而破坏的脆弱测试。

  • 不测试边缘情况:确保测试涵盖广泛的输入,包括边缘情况,以防止在不太常见的情况下bugs

  • 忽略测试可维护性:测试应该像生产代码一样干净且可维护。使用描述性名称和结构测试以便于理解和修改。

  • 缺乏持续集成:将 TDD 与 CI/CD 管道集成以尽早发现问题并确保频繁运行测试。

  • 平衡不同级别的测试(单元、集成、系统)。

  • 定期重构并以与生产代码相同的方式对待测试代码。

  • 增量编写测试并关注代码的行为。

  • 经常运行测试并将其集成到您的 CI/CD 工作流程中。

  • 审查和维护测试以保持其有效性和相关性。

TDD 如何与其他软件开发方法结合使用?

TDD 可以与各种软件开发方法无缝集成,以提高其有效性并从一开始就确保质量保证。 在敏捷环境中,TDD 通过允许为少量功能增量编写测试来补充迭代开发,确保每个迭代 生成通过所有测试的潜在可交付产品。这种协同作用通过提供有关代码更改的即时反馈来支持持续集成和交付。 通过**Scrum,TDD 通过在开发开始之前将验收标准定义为测试来与冲刺保持一致。这确保了冲刺的目标得以实现,并且开发的功能得到了充分的测试,从而通过可演示的工作软件为冲刺评审做出了贡献。 在 极限编程 (XP) 中,TDD 是一种核心实践。它与 XP 对频繁发布和简单性的强调相吻合,确保代码在短周期内经过彻底测试和重构,从而提高代码质量和可维护性。 对于看板**,TDD 提供了一种保持流程效率的方法。通过防止缺陷向下游转移,TDD 有助于减少与 bug 修复和返工相关的瓶颈,从而支持看板对连续流程的关注。 在精益软件开发中,TDD 通过在开发过程的早期预防缺陷来帮助消除浪费。这种主动方法通过避免后期缺陷修复的额外成本和延迟,符合精益原则。 将 TDD 与这些方法相集成需要转变思维方式以优先考虑测试并致力于维护一套强大的自动化测试。通过这样做,团队可以在不同的开发实践中利用 TDD 的优势,从而增强整体 软件质量 和团队敏捷性。

有哪些工具和框架可用于 TDD?

多种工具和框架有助于跨不同编程语言和平台的 TDD:

  • JUnit (Java):广泛使用的单元测试框架。

  • NUnit (C#):与 JUnit 类似,但适用于.NET 环境。

  • 测试NG (Java):提供更高级的功能,例如注释、参数化测试以及对数据驱动测试的支持。

  • R规格 (Ruby):一个专注于 BDD 的工具,提供了一种可读的语言来描述测试。

  • 摩卡 (JavaScript):灵活并支持异步测试,通常与 Chai 等断言库一起使用。

  • Jest (JavaScript):在 React 应用程序中很受欢迎,包括快照和交互式观看模式的功能。

  • pytest (Python):支持简单的单元测试和复杂的功能测试。

  • x单位 (.NET):.NET Framework 的开源单元测试工具。

  • PHP单元 (PHP):面向程序员的 PHP 测试框架。

  • (Swift):适用于 Swift 和 Objective-C 的 BDD 框架。 Java 中 JUnit 的用法示例:

  import static org.junit.Assert.assertEquals;
  import org.junit.Test;
  public class CalculatorTest {
      @Test
      public void testAddition() {
          Calculator calculator = new Calculator();
          assertEquals(5, calculator.add(2, 3));
      }
  }

这些工具通常与 CI/CD 管道集成,从而在构建和部署过程中实现自动化测试执行。选择正确的工具取决于语言、项目要求以及个人或团队偏好。

  • JUnit (Java):广泛使用的单元测试框架。

  • NUnit (C#):与 JUnit 类似,但适用于.NET 环境。

  • 测试NG (Java):提供更高级的功能,例如注释、参数化测试以及对数据驱动测试的支持。

  • R规格 (Ruby):一个专注于 BDD 的工具,提供了一种可读的语言来描述测试。

  • 摩卡 (JavaScript):灵活并支持异步测试,通常与 Chai 等断言库一起使用。

  • Jest (JavaScript):在 React 应用程序中很受欢迎,包括快照和交互式观看模式的功能。

  • pytest (Python):支持简单的单元测试和复杂的功能测试。

  • x单位 (.NET):.NET Framework 的开源单元测试工具。

  • PHP单元 (PHP):面向程序员的 PHP 测试框架。

  • (Swift):适用于 Swift 和 Objective-C 的 BDD 框架。

高级概念

模拟对象在 TDD 中的作用是什么?

模拟对象通过以受控方式模拟真实对象的行为,在 测试驱动开发 (TDD) 中发挥着至关重要的作用。当实际对象由于以下原因无法纳入测试时使用它们:

  • 编写测试时对象不存在

  • 设置复杂度高或困难

  • 性能缓慢会阻碍测试执行

  • 网络或数据库依赖性使测试不太可靠或确定性在 TDD 中,测试是在生产代码之前编写的。模拟允许独立于其依赖项来测试代码单元。这在遵循红-绿-重构循环时尤其重要,因为它使开发人员能够专注于业务逻辑,而不必担心初始阶段的集成部分。 通过使用模拟对象,您可以:

  • 指定预期的交互在测试中使用模拟,定义它应该如何调用以及它应该返回什么。

  • 验证被测系统是否交互按预期进行模拟,确保使用正确的参数调用正确的方法。

  • 测试不同场景通过配置模拟来返回各种输出或抛出异常,这有助于实现彻底的测试覆盖率。 模拟对象对于维护可以频繁运行的快速可靠的测试套件至关重要,这是 TDD 的基石。它们有助于确保每个测试仍然专注于单个功能,并且整个套件可以快速、确定地运行。

  • 编写测试时对象不存在

  • 设置复杂度高或困难

  • 性能缓慢会阻碍测试执行

  • 网络或数据库依赖性使测试不太可靠或确定性

  • 指定预期的交互在测试中使用模拟,定义它应该如何调用以及它应该返回什么。

  • 验证被测系统是否交互按预期进行模拟,确保使用正确的参数调用正确的方法。

  • 测试不同场景通过配置模拟来返回各种输出或抛出异常,这有助于实现彻底的测试覆盖率。

TDD 如何处理复杂系统和依赖项的测试?

TDD 通过强调组​​件的增量开发隔离来处理复杂的系统和依赖关系。对于复杂的系统,在实现相应的代码之前,会针对小的、可管理的功能块编写测试。这种方法确保每个组件在集成到更大的系统之前都经过彻底的隔离测试。 使用模拟存根来管理依赖关系,以模拟复杂的依赖模块的行为。这使得开发人员可以编写专注于感兴趣的单元的测试,而不受外部因素的影响。例如:

  // Example of using a mock object in a test
  it('should call the dependency method', () => {
    const mockDependency = { dependencyMethod: jest.fn() };
    const systemUnderTest = new SystemUnderTest(mockDependency);
    systemUnderTest.performAction();
    expect(mockDependency.dependencyMethod).toHaveBeenCalled();
  });

通过使用模拟,测试可以验证与依赖项的交互,而不需要存在实际的实现。当处理外部服务 数据库 或在 测试环境 中不易控制或复制的其他系统时,此技术特别有用。 对于 TDD 上下文中的集成测试,开发人员可以使用合约测试来确保系统不同部分之间的交互遵循商定的接口。这有助于在开发周期的早期发现集成问题。 总体而言,TDD 的迭代性质与模拟和契约测试的使用相结合,允许对复杂系统及其依赖项进行有效的管理和测试。

什么是行为驱动开发 (BDD) 以及它与 TDD 有何关系?

行为驱动开发 (BDD) 是测试驱动开发 (TDD) 的扩展,强调软件项目中开发人员、QA 以及非技术或业务参与者之间的协作。 BDD 专注于通过对话和具体示例获得对所需软件行为的清晰理解,然后将其转化为一组自动化测试,通常以类似自然语言的格式表达。 BDD 与 TDD 相关,因为它还促进在编写实现功能的代码之前编写测试。然而,虽然 TDD 的测试是基于开发人员的角度,并且通常是在单元级别,但BDD 的测试是从用户的角度派生的,更多地是关于系统的行为。这些测试通常称为“场景”或“规范”,并以特定于领域的语言编写,可转换为自动化测试。 以下是 BDD 场景的示例:

  Feature: User login
    Scenario: Successful login with valid credentials
      Given the user is on the login page
      When the user enters valid credentials
      Then the user is redirected to the homepage

BDD Cucumber 或 SpecFlow 等工具解释这些场景并将它们链接到底层测试代码。这些场景促进利益相关者之间的沟通,并确保各方对功能及其预期行为有共同的理解。这种一致性有助于防止误解,并确保构建的软件符合业务的需求和期望。

什么是验收测试驱动开发 (ATDD) 以及它与 TDD 有何关系?

验收测试驱动开发 (ATDD) 是一种方法,团队通过示例协作讨论验收标准,并在开发开始之前将其提炼为一组具体的验收测试。这是一种协作实践,用户、测试人员和开发人员定义自动验收标准。 ATDD 确保所有利益相关者对需求有共同的理解。 ATDD 与 TDD 密切相关,但是 TDD 侧重于 单元测试 的开发人员视角,而 ATDD 更多的是关于客户和系统的功能。在 ATDD 中,验收测试是根据用户故事创建的,这些测试指导整个开发过程,就像 TDD 中的单元测试一样。 以下是 ATDD 对 TDD 的补充:

  • TDD:编写失败的单元测试,使其通过,重构。

  • ATDD :编写失败的验收测试,实现功能(对单元使用 TDD),使验收测试通过,重构。 ATDD 通常涉及在编写代码之前为用户故事创建详细的自动化测试,而 TDD 是为一小部分功能(通常在类或方法级别)编写测试,然后编写代码以通过测试。这两种做法都旨在确保代码库稳健且无回归,但 ATDD 将验证扩展到功能或系统级别,确保软件满足业务需求。

  • TDD:编写失败的单元测试,使其通过,重构。

  • ATDD :编写失败的验收测试,实现功能(对单元使用 TDD),使验收测试通过,重构。

在 TDD 中处理遗留代码有哪些策略?

使用 TDD 处理遗留代码时,请考虑以下策略:

  • 首先编写特征测试以捕获系统的当前行为。这些测试充当未来变化的安全网。
    test('characterize legacy function behavior', () => {
      expect(legacyFunction(input)).toEqual(expectedOutput);
    });
  • 识别代码中的接缝,您可以在其中引入测试而不改变行为。接缝是您可以更改代码行为而无需在该位置进行编辑的地方。

  • 谨慎重构以避免破坏现有功能。进行小的增量更改并经常运行测试。

  • 使用 Sprout Method 添加新功能。用新方法编写新代码,您可以使用 TDD 进行测试,而不是直接更改旧代码。

  • 当您需要更改旧代码时,应用换行方法。创建一个委托给旧代码的包装器,然后逐渐将功能移至新包装器中,并进行测试。

  • 使用模拟或存根隔离外部依赖项以隔离测试代码。

  • 优先考虑测试覆盖率 的高风险或更改频率的领域,以最大限度地提高您的工作价值。

  • 让利益相关者参与来了解遗留系统的预期行为,确保您的测试反映真实世界的使用情况。

  • 教育您的团队随着遗留系统的发展,维护新测试和遵循 TDD 实践的重要性。 通过集成这些策略,您可以将 TDD 的优势带入遗留系统,提高其 可维护性 和可靠性。

  • 首先编写特征测试以捕获系统的当前行为。这些测试充当未来变化的安全网。

    test('characterize legacy function behavior', () => {
      expect(legacyFunction(input)).toEqual(expectedOutput);
    });