四、测试 JavaScript

学习目标

在本章结束时,你将能够做到以下几点:

  • 分析测试的好处
  • 解释代码测试的各种形式
  • 构建的代码测试环境
  • 为 JavaScript 代码实现测试

本章将涵盖测试、测试框架的概念,以及如何使用不同的方法来有效地测试代码。

简介

在第一章中,我们介绍了 ES6 中发布的许多新的强大特性。 我们讨论了 JavaScript 的演变,并重点介绍了 ES6 中增加的关键功能。 我们讨论了作用域规则、变量声明、箭头函数、模板字面量、增强的对象属性、解构赋值、类和模块、编译以及迭代器和生成器。 在第二章中,我们介绍了 JavaScript 的异步编程范例。 我们讨论了 JavaScript 的事件循环、回调、承诺和 async/await 语法。 在第三章中,我们学习了文档对象模型(DOM)、JavaScript 事件对象和 jQuery 库。

在本章中,我们将学习 JavaScript 中的测试代码和代码测试框架。 在第一个主题中,我们将介绍测试并讨论测试驱动开发。 然后,我们将讨论应用测试驱动开发以及几种不同的测试代码和应用的方法。 在最后一个主题中,我们将讨论几个 JavaScript 代码测试框架,您可以使用它们为代码构建强大的测试。

测试

测试代码很像去健身房,你知道这对你有好处。 所有的争论都是有道理的,但是站起来开始健身是困难的。 最初的冲击感觉很棒; 然而,紧随其后的是肌肉酸痛,你开始怀疑这是否真的值得。 你从一天中抽出一个小时或更多的时间,但你所能表现出来的只是胳膊和腿的酸痛。 但是,几周后,事情就变得容易了。 你开始注意到锻炼的好处。

就像去健身房一样,您可能听说过测试代码有多重要。 编写测试是编写良好且可持续的代码的重要组成部分。 当您第一次开始编写测试时,这可能会很困难。 编写第一个测试并让它们成功运行会带来兴奋或兴奋,但在从工作日中抽出一两个小时来编写测试之后,您开始怀疑是否真的值得这样做。 但是你要坚持下去。 几周后,它变得不那么乏味了,您开始注意到测试代码带来的小好处。

在本章中,我们将讨论测试代码的原因,可能需要实现的测试类型,以及可能用于实现和运行测试的一些 JavaScript 框架。

测试代码的原因

测试代码的原因有很多。 这些原因包括程序正确性、敏捷开发、代码质量、bug 捕获、法律责任、满足感等等。 我们将简要讨论列出的每一个原因,并解释它们的好处。

  1. Correctness

    测试代码最简单和最重要的原因是测试代码检查代码的正确性。 智能编写的测试将根据预定的输入值及其相应的输出值测试代码中的所有逻辑。 通过比较程序的输出和预期的输出,我们可以验证代码是否按预期工作,在将它们集成到代码中之前捕获语义或语法错误。

  2. Agile Development

    测试代码使开发过程更加敏捷。 敏捷开发周期是目前最流行、最热门的开发风格之一,被洛克希德·马丁、Snapchat 和谷歌等软件公司采用。 敏捷开发依赖于短期目标。 更改旧的和经过测试的代码是一个非常缓慢的过程。 如果任何旧代码需要重构,或者需要添加或删除功能,我们就需要重新进行整个测试过程。 通过编写代码测试,我们可以自动化它们,加快测试过程,并比手工测试节省数小时的时间。 这可能是实现敏捷冲刺目标和错过最后期限之间的区别。

    注意:

    敏捷开发周期关注于设计、实现和发布新特性的短时间冲刺。 这些冲刺通常持续两到三周。 这种短而快速的开发策略允许您以较小的部分构建大型产品,并管理可能发生变化的需求。

  3. Bug Catching

    测试代码将允许您在开发周期的早期发现 bug。 在集成到产品或模块之前,应该执行测试。 这意味着测试发现的任何错误都将在集成到产品之前被发现并修复。 调试一个已经完全集成到应用中的模块要比调试一个仍在开发中的模块困难得多。 在集成之前编写并运行测试将允许您在这些错误与其他代码交互之前找到并修复它们,从而节省大量时间。 在集成之前捕获错误并推送正确的工作代码是开发人员可以拥有的最重要的技能之一,而代码测试可以极大地提高这一技能。

  4. Code Quality

    代码测试可以提高编写的代码的质量。 在使用测试编写代码时,我们必须在设计和实现代码时明确考虑到这些测试。 编写好的测试有助于我们更全面地思考我们试图解决的问题和我们将要解决问题的方式; 我们必须考虑诸如边缘情况等问题,并设计一个满足测试需求的良好实现。 编写测试将帮助您更好地理解代码的设计和实现,这将导致更高的质量和更好地考虑代码。

  5. Legal Liability

    编写测试可以帮助预防和减轻法律责任。 在许多司法管辖区和市场领域,要求供应商确保或证明所提供的软件具有可销售的质量。 在某些情况下,文档化的测试过程有可能限制您的法律责任。 这可以防止你因软件错误而被起诉。 在最糟糕的情况下,还可以提供一个记录良好的测试过程,以证明涉及诉讼的软件缺陷不是由于不当操作而产生的。 这可以减少你的惩罚性损害赔偿或个人责任。

  6. Gratification

    测试代码的最后一个原因经常被大多数人忽略。 测试代码是非常令人满意的。 测试可以为您提供关于代码正确性的即时可视化反馈。 看到绿色的勾是非常令人满意的。 发布那些你知道写得很好、测试得很好的代码,并且能够完美地执行的代码是非常令人满意的。 知道你的代码经过了很好的测试可以帮助你在最后期限到来的时候对发布有信心。

测试驱动开发

测试驱动开发(TDD)是一种软件开发形式,专注于在实现代码之前编写测试。 这通常是一个敏捷循环,是将测试集成到代码中的最简单的方法之一。 TDD 是一个围绕一个短而简单的开发周期构建的软件开发过程。 在其最基本的形式中,循环包括添加一个定义新功能应该如何工作的测试,然后编写代码,直到满足测试的需求。 这个循环一直重复,直到添加了所有的功能。

测试驱动开发要求开发人员创建自动化测试。 这些测试应该很好地定义代码的需求,并且应该在编写任何代码之前定义这些测试。 测试应该覆盖所有预期的或潜在的用例,特别是边缘用例。 测试的通过将在开发完成时通知开发人员。

注意:

边缘情况是发生在操作参数的极端情况。 在代码中,边界情况指的是可能需要特殊处理的有效输入值。 例如,如果序列值为 0 或 1,则斐波那契序列算法(F(n)=F(n-1)+F(n-2))需要特殊处理。

TDD 允许开发人员在必要时将代码分解成小的、可管理的步骤。 这是可能的,因为 TDD 要求添加的每个功能和特性都必须有测试。 我们可以编写一个小测试,然后编写使测试通过的代码。 大型特性和功能可以被分解成小块,并以增量方式构建。 这对理解问题的所有部分有很大帮助。

TDD 还可以促进代码的模块化和可重用性。 每一段代码都必须进行测试,并且可以将大的代码段分解为更小的部分。 这将导致更小、更集中的类和函数,以及代码文件之间更少的交叉依赖。 这些较小的部分可以与它们的测试封装在一个模块中,并通过程序共享。 通过运行附加的测试套件,可以简单地验证模块的更新是否正确。

TDD 循环

TDD 循环通常由六个步骤组成:

  1. 添加测试:在 TDD 中,每个新特性都应该从编写测试开始。 要编写一个新的测试,您必须清楚地了解特性的规格说明和需求。 对特性的需求必须经过深思熟虑,并分解成可测试的部分,这些部分可以一次编写一个测试。
  2. 运行所有的测试,看看是否有任何失败:如果要检查新的测试是否通过,测试显然应该失败,因为我们添加的功能尚未实现。 如果测试没有失败,那么该特性已经存在,或者测试编写不正确。 这是对笔试的检查。 测试应该在预期的目的下失败,并有助于检查预期的逻辑是否正在被测试。
  3. 编写代码修复测试:这个阶段的代码不需要完美。 测试可能会以一种低效的方式修复,但这是可以接受的,因为它可以在流程的后期进行重构。
  4. 所有测试都应该通过,包括之前添加的所有测试。 如果新代码破坏了先前通过的测试,则可以恢复更改,以确定破坏更改可能是什么。
  5. 重构/清理代码:如果需要清理代码,可以在这个步骤中完成。 在这里,您可以改进新添加代码的实现,或者修复添加新代码时可能出现的任何测试。 在进行任何重构之后,您应该再次运行测试,以确保所有更改都是正确的。 重复重构并根据需要运行测试步骤,直到重构正确为止。
  6. 重复:添加一个新的测试,并重复 TDD 循环,直到功能完全实现和测试。

测试驱动开发是确保所有代码都经过测试的强大方法,但如果开发人员不认真,它可能会导致一些缺陷。 当需要完整的堆栈或功能测试时,TDD 很难使用。 完整堆栈或功能测试是对技术堆栈的多个部分同时进行的测试。 需要用户界面元素、数据库调用或网络调用的测试可能很难编写。 通常,可以通过使用模拟数据或网络调用来欺骗代码中测试的外部交互。

如果测试不频繁运行或维护不善,TDD 也会开始崩溃。 如果放弃测试,并且从不运行,或者只是偶尔运行,那么 TDD 的整个目的就会崩溃。 添加到程序中的功能在设计时考虑到了测试,而测试用于验证功能是否正确实现。 如果测试从未运行,那么 TDD 的整个目的就会被忽略。 缺乏维护的测试也会阻碍 TDD 的有效性。 糟糕的维护可能是由于没有更新以满足调整后的特性需求,或者是由于没有添加概述新特性需求的新测试。 维护不善的测试将不能正确地告知您所编写的代码是否按照我们希望的方式执行。

TDD 也可能成为糟糕或懒惰编写的测试的牺牲品。 如果测试太粗糙,他们将无法找到代码中的 bug。 测试的编写必须具有足够的特异性,以独立于其他逻辑来测试每一位逻辑。 另一方面,如果添加了一些琐碎的测试,我们就会在 TDD 敏捷过程中浪费时间。 如果编写的测试很琐碎,或者重复了以前的测试,我们将降低开发效率。

最后,如果团队中的任何成员不采用开发策略,TDD 可能会崩溃。 如果在添加新代码之前,只有一部分开发团队编写了测试,那么我们只能测试和验证一小部分代码库。 为了让 TDD 得到最好的结果,它必须被开发团队的所有成员完全采用。

结论

测试代码是确保代码按预期方式运行的最佳方法。 如果您目前没有测试您的代码,那么开始实现测试会非常困难; 然而,这是应该做的。 测试您的代码可以使您的代码更加正确,易于编写,并且具有更高的质量。

测试驱动开发是开始在项目中集成测试的最简单的方法之一。 TDD 的核心是编写测试,在编写任何实现代码之前概述添加的任何特性或功能的需求。 它迫使开发人员准确地理解每个特性将如何实现。 TDD 是一个简单的六步过程:添加测试、运行测试、编写代码、运行测试、重构、重复。 这个过程确保对功能的每个小块进行测试。

练习 24:应用测试驱动开发

你的任务是编写一个斐波那契数列生成器。 使用测试驱动的开发周期来编写测试并开发斐波那契算法。 您可以使用编写在第 1 章:介绍 ECMAScript 6活动 I 的斐波那契代码作为参考(它可能需要也可能不需要修改)。 n=0你应该编写测试的条件,然后实现n=0状态,然后编写测试和实施n=1状态,然后编写测试和实施n=2状态,最后n=5,n=7,n=9条件。 如果测试通过,记录Test passed。 否则,抛出一个错误。

要使用 TDD 开发和测试算法,请执行以下步骤:

  1. 手工计算斐波那契数列在 n=0, n=1, n=2, n=5, n=7, n=9 处的值。
  2. Write a function called fibonacci that recursively calculates the Fibonacci sequence value where the value takes in a variable i and checks if i<=0.

    如果是,返回1,然后检查if i==1

    如果是,则返回1。 否则,它递归地获取斐波那契值。

    然后返回fibonacci(i-1) + fibonacci(i-2)

  3. 编写一个通用测试函数 test,它接受两个参数:一个计算值(value)和一个期望值(expected)。

  4. 检查两个值是否不一致。 如果是,则抛出一个错误。
  5. 如果两个值相同,则打印Test passed消息。
  6. 对于每一个要测试的条件(在步骤 1 中计算,n=0, n=1, n=2, n=5, n=7, n=9),使用test函数对测试条件写一个测试。
  7. 调用test函数,并传入fibonacci函数返回的值和手动计算的值。
  8. 运行测试。
  9. 如果测试失败,修复fibonacci函数中的 bug。
  10. 再次运行测试,直到 bug 被修复。
  11. 如果测试通过,继续执行下一个测试条件。
  12. 如果测试失败,修复 bug 并重新运行测试。

编码

index.js
function fibonacci( i ) {
 if ( i <= 0 ) {
   return 0;
 } else if ( i === 1 ) {
   return 1;
 } else {
   return fibonacci( i - 1 ) + fibonacci( i - 2 );
 }
}
function test( value, expected ) {
 if ( value !== expected ) {
   throw new Error( 'Value did not match expected value' );
 } else {
   console.log( 'Test passed.' );
 }
}
test( fibonacci( 0 ), 0 );
test( fibonacci( 1 ), 1 );
test( fibonacci( 2 ), 1 );
test( fibonacci( 5 ), 5 );
test( fibonacci( 7 ), 13 );
test( fibonacci( 9 ), 34 );

https://bit.ly/2H5CNv0

代码片段 4.1:测试代码

输出

Figure 4.1: Fibonacci test

图 4.1:斐波那契检验

您已经成功地应用了测试驱动开发来开发和测试算法。

T 测试类型

软件测试有许多不同的形式。 在本节中,我们将讨论测试代码的不同方法,并涵盖最常见的代码测试类型。

黑盒和白盒测试

有两种测试代码的方法,黑盒和白盒。 术语黑箱表示内部工作未知的系统。 观察系统的唯一方法是通过它的输入和输出。 一个白盒系统是一个内部工作已知的系统。 可以通过它的输入、输出和精确的内部工作来观察它。 黑盒和白盒系统可以是任何东西,从软件程序,到机械设备,或任何其他系统。

黑盒测试指测试人员不知道代码的内部结构或实现的软件测试。 我们只能观察代码系统的输入和输出。 白盒测试白盒测试是指测试人员知道软件内部结构或实现时的软件测试。 我们能够观察输入和输出,以及程序的每一步内部状态是如何变化的。 几乎所有形式的代码测试都基于黑盒或白盒测试原则。 下图显示了黑盒与白盒的对比:

Figure 4.2: Black box and white box visualization

图 4.2:黑盒和白盒的可视化

我们将讨论三种类型的测试:单元测试功能测试集成测试。 单元测试用于根据预期目的验证所有可测试代码。 它们测试最小的逻辑片段以确保实现的正确性。 功能测试的目的是确认特性或组件的功能。 集成测试的目的是测试集成组件,以验证它们在集成系统中按预期一起工作。 这三种类型的代码测试为您处理测试代码提供了良好的基础。

Unit testing

单元测试是最常见的测试形式之一。 单元测试用于确保功能的特定功能片段满足需求。 单元测试通常是从白盒测试的角度构建的,我们将在本章中讨论单元测试,假设代码的内部功能是已知的。 虽然单元测试可以从黑盒的角度构建,但这更接近于功能测试,将在下一节中详细讨论。

单元测试只是用尽可能小的单元测试代码片段的测试。 代码的“单元”是逻辑上与代码的其他部分隔离的一小部分。 换句话说,它是一段在逻辑上不依赖于代码其他部分的代码。 代码单元可以在不影响周围代码运行的情况下进行更新。 例如,考虑下面的代码片段:

function adjustValue( value ) {
 if ( value > 5 ) {
   value--;
 } else if ( value < -5 ) {
   value++;
 }
 return value
}
代码片段 4.2:代码单元示例

函数adjustValue()接受一个数字。 如果该数大于 5,则减去 1;如果该数小于-5,则加 1。 我们可以将这个代码片段分成三个逻辑单元,如下所示:

  1. 第一个单元是检查值是否大于 5 的if语句和递减运算符(value--)。
  2. 第二个单元是 elseif语句(检查值是否小于-5)和递增操作符(value++)。
  3. 第三个逻辑单元是return语句。 更改这三个逻辑单元中的任何一个都不会影响其周围代码的逻辑结构。

我们可以为每个单元创建一个单元测试,以确保它们能够正确运行。 我们的单元测试每次只测试一个代码单元。 对于本例,我们将需要 3 个单元测试。 我们将构建测试来检查返回值、大于 5 的条件和小于-5 的条件。 要测试返回条件,我们只需要传入一个小于或等于 5 的值和大于或等于-5 的值。 返回的值应该与传递给函数的值相同。 要测试大于 5 的条件,必须传入一个大于 5 的值。 我们知道返回值必须比输入值小 1。 要测试小于条件,必须传入一个小于-5 的值。 我们知道返回的值应该比输入的值高 1。 这三个单元测试可以放在代码文件中,并在我们修改代码后运行。

单元测试应该尽可能频繁地运行。 应该将单元测试放入文件中,并在任何代码逻辑更改时运行。 一段代码的逻辑的微小变化可能导致结果的重大变化。 持续的测试将有助于确保没有小的 bug 从裂缝中爬出来。 许多公司都有自动测试系统,可以在 Git 存储库提交或版本发布上自动运行单元测试。 这种自动化测试非常有助于跟踪破坏代码的提交和更改。 这可以大大减少调试时间和工作量。

练习 25:构建单元测试

您的任务是为一段代码构建单元测试。 要完成这项作业,请遵循以下说明:

  1. 参考exercises/exercise25/exercise.js中提供的文件,并查看fakeRounding函数。 我们将为此功能构建单元测试。
  2. In that file, write a general testing function called test that takes in two arguments: a calculated value (value) and an expected value (expected). Check whether the two values are different. If they are, throw an error.

    如果这两个值相同,则打印测试通过的消息。 如果您愿意,您可以使用练习 24中的测试功能。

  3. Reference the fakeRounding function, line by line, and analyze what the function does to the input and the resultant output.

    它获得传入的数字的绝对值的小数部分。 如果小数点为<=0.5. Next, it returns the input rounded down to the nearest integer if the decimal is >0.5,则返回输入四舍五入到最接近的整数。

  4. Write tests to check the following cases using the test function we created. Calculate the expected value from the provided input.

    为多个输入编写测试,0,0.4999,0.5,0.5001,-0.4999,-0.5,和-0.5001:

编码:

解决方案
test( fakeRounding( 0 ), 0 );
test( fakeRounding( 0.4999 ), 1 );
test( fakeRounding( 0.5 ), 1 );
test( fakeRounding( 0.5001 ), 0 );
test( fakeRounding( -0.4999 ), 0 );
test( fakeRounding( -0.5 ), 0 );
test( fakeRounding( -0.5001 ), -1 );
片段 4.3:单元测试

https://bit.ly/2Fjulqw

输出:

Figure 4.3: Unit Test

图 4.3:单元测试

您已经成功地为一段代码构建了单元测试。

有趣功能测试

功能测试是一种黑盒测试方法,用于确定应用的一个组件是否按照定义的规范工作。 功能测试通常比单元测试更复杂。 单元测试用于测试组件内部功能的逻辑,而功能测试用于测试组件是否满足规格表或数据表中定义的规格。 例如,如果网页上有一个只接受数字的表单,我们可以使用数字和字符串进行功能测试,以确保正确满足“只接受数字”的规范。

功能测试可以分为五个步骤:

  1. 确定的功能
  2. 创建输入数据
  3. 确定输出数据
  4. 比较输入输出
  5. 修正错误

构建功能测试的第一步是确定需要测试的功能。 功能测试通常测试主要功能、错误条件、可用性等。 通过查看特性/组件规范或数据表,通常最容易确定需要构建哪些测试。 您可以从数据表中获取组件所需的程序行为和错误处理,并将其分解为一系列测试。

一旦你决定了什么功能需要测试,以及你将如何测试该功能,你必须创建输入数据来测试。 测试所需的输入数据很大程度上依赖于正在构建的组件或特性,因此很难将其归纳为教科书的目的。 但是,您应该使用您希望程序接受的值和程序可能无法预料的值进行测试。 例如,如果我们正在创建一个电子邮件输入表单,我们应该用有效的电子邮件(xxxx@yyy.zzz)和无效的电子邮件(12344312)来测试输入字段。 在生成任意测试数据时,使用数组、字符串或其他数据结构中的非顺序值进行测试通常是一个好主意。 使用随机值可以帮助您发现逻辑错误。

一旦确定了测试所需的输入数据,就必须计算出该特性的预期输出。 这部分过程可以说是最重要的,不应操之过急。 输出值永远不应该通过将输入输入到正在测试的程序中来计算。 这将导致在运行测试时出现重复语句,并且不会发现任何错误。 我见过许多测试失败,因为程序员没有正确计算预期的输出值,测试无效。

一旦确定了输出值,我们就可以运行测试了。 输入值应该在特性或组件中运行,并与输出值进行比较。 如果组件的输出值与上一步中计算的预期输出值匹配,则测试通过。 如果值不匹配,则表示测试没有通过,需要修复错误。

这个过程的最后一步是修复错误。 如果测试没有通过,那么在组件的某个地方有一个错误。 一旦修复了错误,就可以重新运行测试。 如果测试的所有功能的所有测试都通过,则可以认为组件已准备好集成。

构建测试可能是功能测试中最困难的部分之一。 我们需要构建两种不同类型的测试:阳性测试和阴性测试。 正测试测试预期的程序使用流,负测试测试意外的使用流。

阳性测试相对容易产生。 您可能希望或期望用户执行的任何操作都可以转化为一个积极的测试用例。 例如,单击应用上的按钮或在文本字段中输入信息。 这两个用例可以转换为单击按钮的功能测试和在文本字段中输入的功能测试。 由于阳性测试被设计用于测试预期的程序流,所以它们应该使用有效的和预期的数据。 如果测试不使用数据,而是使用一些其他功能,比如用户的鼠标点击,我们只需要为预期行为编写积极的测试。

阴性测试更难创建。 它们需要更多的创造力来有效地构建和执行,因为你必须想出奇怪的方法来破坏你自己的代码。 通常很难预测用户会如何滥用某个特性。 负面测试旨在测试错误路径和失败。 例如,如果我们想让用户点击我们网站上的一个按钮,为双击条件编写负面测试可能是谨慎的做法。 双击是意外行为,如果没有正确解释,可能会导致表单重新提交。 负面测试对于全面测试一个特性至关重要。

Int 集成测试

集成测试是功能测试的后退一步。 集成测试用于测试模块和组件在完全集成时的工作方式。 单元测试逐个测试功能。 功能测试逐个测试完整的组件或模块。 集成测试测试组合的组件,以确保它们正确地相互交互。 集成测试通常比单元测试或功能测试更复杂。 集成测试可以写一些简单的作为一个个人网页建了曾经的所有组件和集成在一起,这样复杂的东西或一个完整的前端应用有一个 API,多个服务器,数据库一旦准备好所有的单个组件和总和。 集成测试通常是最困难和最耗时的测试形式。

集成测试可以简化,就像生产圆珠笔一样。 笔帽、笔身、墨水、圆珠笔、带夹的尾帽都是圆珠笔的组成部分。 它们都是单独制造和测试的,以确保每个组件符合为其设置的规格。 当组件准备好后,将它们放在一起进行集成测试,该测试将测试组件是否正确地在一起工作。 例如,我们的集成测试可以测试圆点是否适合墨盒,墨水和圆点是否适合笔体,笔帽是否适合笔体。 如果其中一个测试失败,集成系统(圆珠笔)将不能正常工作,必须更新一个或多个组件。

有几种方法可以用于进行集成测试。 它们是大爆炸测试、自底向上测试、自顶向下测试和三明治测试。 每一种都有其优点和缺点。

大爆炸测试包括一次组合所有组件,然后运行测试。 它被称为大爆炸测试,因为你把所有的东西都放在一起,然后(很可能)爆发出失败的集成测试。 大爆炸测试对于没有很多组件间交互的小型系统非常方便。 当应用于大型系统时,大爆炸测试经常会失败。 第一个问题是,在一个非常大和非常复杂的系统中,故障定位会更加困难。 如果找到 bug 的来源需要很长时间,那么我们的测试周期将会非常缓慢。 第二个问题是,由于系统的复杂性,组件之间的一些链接可能会被忽略或没有被测试。 如果有数百个组件链接需要测试,那么如果它们同时被链接,就很难对它们进行跟踪。 大爆炸测试的第三个缺陷是,集成测试在所有模块或组件都设计好并完全构建好之后才能开始。 由于您必须一次组合所有的模块,因此一个模块的延迟将推迟整个系统的集成测试。

第二种形式的集成测试是自下而上测试。 在自下而上的测试中,我们必须将系统的层次结构想象成一棵树。 我们首先从集成底层模块开始。 然后,一旦所有测试通过,我们添加下一层模块或组件,直到我们测试了整个系统。 要以这种方式进行测试,我们必须使用驱动程序来模拟上层,并调用正在测试的底层中的模块或组件。 驱动程序只是一些代码,用来模拟高级模块以及它们为了测试的目的对低级模块的调用。 自下而上的测试有两个主要好处。 首先,故障定位非常容易。 模块从最低层向上集成。 如果一个新集成的模块出现故障,那么我们可以迅速找出并指责需要修复的模块。 第二个好处是不必浪费时间去等待所有模块的开发。 如果模块也是用自底向上的方法开发的,那么一旦它们准备好了,我们就可以简单地将它们添加到集成测试中。 我们可以在组件就绪时进行集成测试,而不是等到整个系统构建完成。 自下而上的测试有两个主要缺点。 首先,我们很难创造一个早期的原型。 由于模块是自下而上构建和集成的,面向用户的特性和模块通常是最后实现和测试的。 因为原型组件通常是最后才准备好,所以早期原型很难制作。 第二个缺点是,控制应用流程的顶层关键组件和模块是最后测试的,可能不会像首先测试的模块那样全面。 对于大型集成系统,我通常认为自下而上的测试比大爆炸测试更好。

第三种形式的集成测试是自顶向下测试。 在自顶向下测试中,我们必须将系统层次想象成一棵树。 我们首先从集成系统的顶层开始。 这些通常是面向用户的组件和程序流程模块。 自顶向下测试要求测试人员构建存根,以在较低级别模拟模块的功能。 存根模仿未开发的模块,以便被测试的模块可以进行它们需要的调用。 自顶向下测试有三个主要优点。 与自下而上测试一样,第一个主要优点是故障定位非常容易,我们不需要等待整个系统构建完成后才能开始集成测试。 一旦组件构建完成,就可以一次添加一个组件。 自顶向下测试的第二个好处是,可以很容易地创建早期原型。 面向用户和最关键的组件是首先构建和测试的,所以很容易将它们集成到早期演示的原型中。 最后一个主要优点是关键模块是按优先级进行测试的。 首先构建关键模块,因此测试更频繁,通常更全面。 自顶向下测试有两个主要缺点。 第一个是需要很多存根。 较低级别的每个模块或组件必须构建到存根中进行测试。 这可能需要编写大量额外的代码。 第二个缺点是,较低级别的模块是最后构建和测试的。 一般来说,它们没有经过彻底的测试。

集成测试的最终形式是三明治测试夹层测试是一种自上而下和自下而上方法的结合。 最重要和最低级别的模块是同时构建和集成的。 这种方法的优点是提供了一种更通用的、大爆炸式的集成测试方法,同时保持了自顶向下和自底向上测试的优点。 三明治测试的最大缺点是存根和驱动程序都需要构建。 如果系统非常复杂,有时很难理解什么是存根或驱动程序。

Bui 测试

构建测试似乎是一个非常艰巨的过程。 从零开始设计一个完整的测试套件是非常困难的。 然而,测试驱动开发为我们提供了一个创建测试的良好起点。 如前所述,在测试驱动开发部分中,构建测试应该总是从编写需求表开始。

需求表是功能、特性或正在构建的整个系统的数据表。 需求表应该将特性的需求分解成非常详细和具体的列表。 为软件应用编写需求表超出了本书的范围,但是我们将通过一个简短的示例来完成。 假设我们的任务是构建一个 facebook 风格的评论创建组件。 该组件必须有一个具有字符限制的文本字段和一个发布评论的按钮。 在这个场景中,我们可以很容易地构建两个通用需求,一个是文本字段的字符限制,另一个是在单击事件后进行 API 调用的按钮。 然后可以将这两个需求细化为以下需求列表:

  1. 文本字段必须接受用户输入的字符。
  2. 当文本字段包含 250 个或更多字符时,不能向文本字段添加任何字符。
  3. 按退格键可以删除文本域中的任何字符。
  4. 按钮必须响应一个onclick事件。
  5. 在单击事件上,组件必须使用测试字段数据调用 API。

这并不是该特性或特性中的组件的完整需求列表,但对于本例来说,这已经足够了。 有了这些需求,我们就可以开始编写测试了。

我们可以开始编写测试,一项一项地完成需求列表。 每个需求应该分解为一个或多个测试。 每个测试都应该准确地测试一件事,并有一个非常具体的成功标准。

第一个要求是文本区域必须接受用户输入的字符。 如果我们按下键盘上的一个键,按下的字符应该被添加到文本区域,因此我们的第一个测试应该是按下键盘上的一个键,并验证相同的字符被添加到文本区域。

第二个要求是,当文本字段包含 250 个或更多字符时,不能向文本字段添加任何字符。 这可以分为两种测试:当文本区域有 250 个字符时,不能向文本区域添加按键;当文本区域有超过 250 个字符时,不能向文本区域添加按键。

第三个要求是,可以通过按退格键删除文本字段中的任何字符。 这个要求可以很容易地转换为测试。 我们必须测试如果按下退格键,将从文本区域删除一个字符。 为了正确地测试边缘用例,我们应该运行四次测试:一次是空白文本区域,一次是大于 0 但小于 250 个字符的文本区域,一次是 250 个字符,一次是大于 250 个字符的文本区域。 测试我们的文本区域的所有操作条件(即使是超过 250 个字符的测试用例,我们从来没有期望达到)将确保不会发生失败。

第四个要求要求按钮必须响应单击事件。 这个测试很容易编写。 我们只需要在用户单击按钮的地方添加一个测试。 最后一个要求是,按钮上的单击事件必须调用 API。 通过模拟点击事件,确保网站使用正确的数据进行 API 调用,我们可以轻松地将其转换为测试。

我们在一系列测试中列出了五个要求。 现在可以将这些测试编译在一起,并在测试文件中以代码形式编写。 这个测试文件将用于验证我们需求表中列出的需求是否得到了适当的满足。

Exercise 26: Writing Tests

您的团队已被指派为您的通讯建立一个注册页面。 注册页面必须有三个文本字段,包括姓名、电子邮件和年龄,以及一个提交按钮。 您的注册页面必须接受 1 - 50 个字符(含)的姓名,1 - 50 个字符(含)的电子邮件(含,电子邮件格式未验证),以及用户的年龄(必须大于 13 岁)。 当按下提交按钮时,必须对用户信息进行验证(参照上一节提供的规范)。 如果不符合规范的任何部分,在浏览器控制台中抛出一个错误。 编写一个非常基本的规格说明表,详细说明每个输入和提交按钮的需求,然后根据规格说明表构建测试。 实现页面(使用exercises/exercise26/exercise.html作为起点)并从 UI 手动执行测试。 启动文件包含您必须编写的测试的提示。 在打开启动文件之前编写规范表和测试。

要生成基本规格表并从规格表运行测试,请执行以下步骤:

  1. 通过将场景描述中包含规格信息的每个句子分解为一个或多个需求来编写规格说明表。
  2. 将规格表分解为手动 UI 测试,方法是获取规格表上的每一项,并为其编写一个或多个测试。
  3. exercises/exercise26/exercise.html处打开启动器 HTML 文件。
  4. Add the three input fields with the IDs name, email, and age. This is shown in the following figure:

    Figure 4.4: Data Sheet (After step 4)

    图 4.4:数据表(步骤 4 后)
  5. 在 HTML 文档中添加Submit按钮,并让它在点击时调用validate函数。

  6. In the validate function, get the name text field by email id and save its value in the name variable.

    通过 id 获取email文本字段,并将其值保存在email变量中。

    通过 id 获取age文本字段,获取其值,解析数值,然后将解析后的值保存到age变量中。

    检查与name领域相关的规格表上的条件。 还要检查名称是否不存在或为 false,如果不存在则抛出一个错误。 检查是否name length <= 0 or > 50,如果是,则抛出错误。

    检查与email领域相关的规格表上的条件。 同时,检查邮件是否不存在,或者是假的; 如果是,则抛出一个错误。 检查是否email length is <=0 or > 50,如果是,则抛出错误。

    检查与age领域相关的规格表上的条件。 另外,检查年龄是否不存在,或者是错误的; 然后抛出一个错误。 检查是否age < 13,如果是则抛出一个错误。

    将用户详细信息(nameemailage)记录到控制台。

  7. For each test you wrote in the specification sheet, test it manually. Fill in the values in the text fields and then click Submit.

    将控制台记录的错误与测试的预期结果进行比较。

    如果测试失败,那么更新验证函数以修复错误并重新运行测试。

编码

solution.html
<body>
 <input type="text" id="name" value="Name">
 <input type="text" id="email" value="Email">
 <input type="text" id="age" value="Age">
 <button onclick="validate()">Submit</button>
 <script>
   function validate() {
     const name = document.getElementById( 'name' ).value;
     const email = document.getElementById( 'email' ).value;
     const age = parseInt( document.getElementById( 'age' ).value, 10 );
     if ( !name ) {
       throw new Error( 'Must provide a name.' );
     } else if ( name.length <= 0 || name.length > 50 ) {
       throw new Error( 'Name must be between 1 and 50 characters.' );
     }
     if ( !email ) {
       throw new Error( 'Must provide an email.' );
     } else if ( email.length <= 0 || email.length > 50 ) {
       throw new Error( 'Email must be between 1 and 50 characters.' );
     }
     if ( !age ) {
       throw new Error( 'Must provide an age that is also a number.' );
     } else if ( age < 13 ) {
       throw new Error( 'Age must be at least 13.' );
     }
     console.log( 'User details:
     Name: ${name}
     Email: ${email}
     Age: ${age}' )
   }
 </script>
</body>
代码片段 4.4:测试前端输入代码

https://bit.ly/2H5E7OJ

输出

Figure 4.5: Data Sheet (Final Output)

图 4.5:数据表(最终输出)

您已经成功构建了基本规格表并从规格表运行测试。

测试工具和环境

测试工具、框架和环境的设计是为了使测试代码更简单、更快。 JavaScript 有很多测试框架,下面简单介绍一下最流行的。 然后我们将深入研究其中一个框架,并演示如何使用该框架编写良好的测试。

【文本工人】ng 企业

您需要根据希望执行的测试类型选择测试框架。 JavaScript 通常有三种测试方式:通用测试代码覆盖测试用户界面测试。 在选择框架时,您必须决定要测试什么以及希望如何进行测试。

常规测试包括单元测试、功能测试和集成测试。 这是对你的测试的一种概括。 目前最流行的测试框架有MochaJasmineJest。 Jest 是 Facebook 使用的,也是最简单的框架之一。 Mocha 是最流行的 JavaScript 测试框架,本节稍后将详细介绍。

代码覆盖测试用于帮助检查测试的完整性。 代码覆盖率可以定义为由自动化测试覆盖或测试的代码基的百分比。 代码覆盖率可以用作代码测试完整性的一般指导原则。 从理论上讲,应用的代码覆盖率越高,测试就越完整、越好。 然而,在实践中,拥有 100%的代码覆盖率并不意味着代码测试是经过深思熟虑的和有效的。 它只是意味着在测试中每个代码路径都以某种方式被引用。 编写经过深思熟虑的测试要比把无用的测试放在一起,只写一行代码更重要。 最流行和最简单的代码覆盖库是Istanbul。 它与许多测试框架兼容,可以很容易地工作到大多数测试套件中。 如果您需要一个第三方库来测试代码覆盖率,我推荐使用 Istanbul。

测试的最终形式是用户界面(用户界面)测试。 与一般测试一样,我们可以将 UI 测试分解为集成测试、功能测试和单元测试。 但是,UI 测试通常不包括在常规测试中,因为它们需要特殊和更复杂的框架。 要执行 UI 测试,我们必须加载用户视图并模拟用户交互。 一些更常见的 UI 测试框架有 Testcafe、WebdriverIO 和 Casper。

摩卡

Mocha是一个在 Node.js 中测试 JavaScript 的框架。 它是一个简单的库,旨在简化和自动化测试过程。 摩卡的设计简单、灵活、可扩展。 我的公司使用摩卡进行单元、功能和集成测试。 我们将讨论相对于其他框架使用 Mocha 的一些好处,介绍如何设置和运行使用 Mocha 的首次测试,并解释 Mocha 提供的一些高级功能。

请注意

摩卡的完整文档可以在https://mochajs.org/上找到。

摩卡咖啡有很多好处。 如前所述,Mocha 是最流行的 Node.js 测试框架。 这立刻给了摩卡最大的优势:摩卡拥有最大的开发社区。 这对于支持和扩展非常重要。 如果您的 Mocha 测试遇到问题,这个社区可以提供广泛的支持。 Stack Overflow 社区会及时回答有关摩卡的问题。 Mocha 社区还为独特的测试场景构建了许多插件或扩展。 如果你的项目有独特的测试需求,很可能已经构建了一个插件来满足你的需求。

除了大型社区支持外,Mocha 还提供了简单的设置、断言和简单的异步测试等优点。 可以通过 npm 的命令行来设置 Mocha。 对于任何测试框架,我们都希望确保设置它不会占用我们太多的时间。 Mocha 还允许使用断言模块。 如果您的团队希望从断言标准来进行测试,虽然这不是必需的,但是 Mocha 允许您安装和导入许多 JavaScript 断言库。 最后,Mocha 是为异步测试设计的。 对于任何 JavaScript 测试模块,我们都必须依赖异步支持来编写完整的测试。 Mocha 被设计用于回调、承诺和 ES6 异步/等待语法。 它可以很容易地集成到大多数后端设置中。

Setting Up 摩卡

使用 npm 命令npm install -g mocha安装 Mocha。 这个命令将在系统上全局安装 Mocha。 任何 Node.js 项目现在都可以使用 Mocha 来运行测试了。 在全局安装后,我们将能够使用mocha关键字从命令行运行测试。

一旦在我们的系统上安装了摩卡,我们必须将它添加到一个项目中。 如果你没有 Node.js 项目,创建一个到所需项目目录的路径,并用npm init初始化项目。 当我们讨论 transpiling and Babel 时,在第 1 章中使用了相同的命令来建立一个项目。 命令将创建一个名为package.json的文件。 在创建了 JavaScript 项目之后,我们需要创建项目文件。 创建一个文件index.js和一个文件test.jsindex.js包含我们的项目代码,test.js包含我们的测试代码。

package.json文件中,有一个字段叫scripts。 要从 npm 运行测试,必须向scripts对象添加一个字段。 用如下代码片段所示的代码替换scripts对象:

"scripts": {
  "test": "mocha ./test.js"
}
代码片段 4.5:package.json 中的测试脚本

上述代码片段中的代码将一个名为test的脚本添加到package对象。 我们可以使用npm run test命令运行这个脚本。 当我们运行这个命令时,它调用带有./test.js参数的mocha关键字。 mocha 测试框架与test.js文件中包含的测试一起运行。 现在我们可以开始向test.js添加测试了。

摩卡用describeit关键词组织测试。 这两个函数都接受字符串作为第一个参数,接受函数作为第二个参数。 describe函数用于将测试分组在一起。 函数用来定义一个测试。 describe()的函数参数包含测试声明(带有 it())或更多描述函数。 it()的函数参数包含要运行的测试函数。

您可以将描述函数看作是描述一组测试并将其分组的一种方法。 例如,如果我们有一组测试,它们都测试一个名为calculateModifier的函数,我们可以使用描述函数describe( 'calculateModifier tests', () => { ... } )将这些测试与一个描述组合在一起。 这将函数中包含的测试分组到描述calculateModifier``test下。

你可以把it函数看作是一种以“it should…”的形式定义测试的方法。 输入到it函数的字符串描述了测试,通常是测试试图完成的内容。 函数参数包含实际的测试代码。 例如,如果我们想要定义一个检查两个值是否相等的测试,我们可以使用it function来做这件事:it( 'should have two inputs that are equal', () => { ... } )。 描述告诉我们应该发生什么,检查值的代码将放在函数参数中。

摩卡基础

了解测试的基础知识后,我们可以查看 Mocha 启动器文档,并查看以下代码片段所示的代码:

var assert = require('assert');describe('Array', function() {  describe('#indexOf()', function() {    it('should return -1 when the value is not present', function() {      assert.equal([1,2,3].indexOf(4), -1);    });  });});
片段 4.6:摩卡基础

您认为这个代码片段在做什么? 首先,我们用Array描述一组测试。 在第一个describe块的函数参数中,我们有另一个describe块。 这个新块描述了一组测试#indexOf; 因为这些描述块是嵌套的,所以我们可以假设我们正在测试数组的indexOf功能。 在第二个describe块中,我们用it函数定义了一个测试。 我们定义一个测试,说it should return -1 when the value is not present。 正如测试描述中所预期的那样,如果数组中没有这个值,我们会期望indexOf函数返回-1值。 在本例中,我们使用 assert 库断言-1的期望值等于实际值。 assert 库并不是严格必需的,但它使这个示例更容易理解。

练习 27:建立摩卡测试环境

目的是建立一个摩卡测试环境,并准备一个测试文件。 要完成这项任务,请遵循以下步骤:

  1. 运行npm``init命令在“exercise”目录下创建一个package.json文件。
  2. 运行npm``install mocha -g安装测试包。
  3. 创建一个名为test.js的文件,用于测试。
  4. package.json文件添加一个脚本,用于在test.js文件上运行 mocha 测试套件。
  5. test.js文件中,添加一个describe()块,将测试描述为My first test!
  6. describe块的回调函数中,添加一个带有it()的测试,该测试通过并具有Passing test!描述
  7. 通过调用添加到package.jsonnpm脚本运行测试。

编码:

test.js
describe( 'My first test!', () => {
 it( 'Passing test!', ( done ) => done( false ) );
} );
片段 4.7:摩卡基础

https://bit.ly/2RhzNAy

输出:

Figure 4.6: Mocha Testing

图 4.6:摩卡测试

您已经成功地建立了一个 Mocha 测试环境并准备了一个测试文件。

Mocha Async

Mocha 支持异步测试和同步测试。 在代码片段 4.6 所示的示例中,我们执行同步测试。 为了支持异步测试,我们需要做的就是将 done 回调参数传递给it()函数的函数参数:it( 'description', ( done ) => {} )。 这告诉 mocha 等待,直到done回调被调用,然后继续进行下一个测试。 done参数是一个函数。 如果测试成功,应该用falsy值调用 done(没有错误)。 如果用truthy值调用 done,摩卡会将该值解释为错误。 最好的做法是将一个错误对象传递给 done 回调,但任何计算值为 true 的值都将告诉 Mocha 测试失败。

Mocha 按照在测试文件中定义的顺序同步执行异步测试。 测试可能会异步地查询资源,但在下一个测试将不会开始运行,直到前一个测试完全完成(已调用 done)。 同步运行测试很重要。 尽管同步运行测试可能会导致更长的测试时间,但它允许我们测试可能依赖于某些共享状态的异步系统。 例如,我们可以用 Mocha 测试数据库和数据库接口等系统。 如果我们需要执行一个集成测试来测试向数据库添加和从数据库中删除的过程,那么我们可以创建一个测试来向数据库添加项目,并创建一个测试来从数据库中删除添加的项目。 如果测试以异步方式运行这两者,我们可能会遇到时间问题。 由于网络延迟或其他意外错误,删除操作可能在添加操作之前处理,测试将失败。 通过强制测试同步运行,Mocha 防止了此类调试问题的需要。

摩卡钩

对于更复杂的测试,Mocha 允许我们将钩子附加到测试上。 挂钩可用于设置测试的前置条件和后置条件。 简单地说,钩子允许我们在测试之前进行设置,并在测试之后进行清理。 摩卡提供以下钩:beforeafterbeforeEachafterEach。 钩子接受两个参数,一个description函数参数和一个callback函数参数。 这个函数参数可以接受一个参数—done 函数。 下面的代码片段显示了钩子的语法示例:

description ('Array', () => {

  before( 'description', done => { ... } );
  after( 'description', done => { ... } );
  beforeEach( 'description', done => { ... } );
  afterEach( 'description', done => { ... } );
} );
片段 4.8:摩卡钩子

钩子只在包含它们的描述块中的测试之前或之后运行。 钩子在任何已定义的测试开始之前运行一次。 它们可用于设置测试之间的通用共享状态。 在每次测试开始之前,beforeEach钩子在describe块中运行。 它们可用于设置或重置每个测试所需的共享状态或一组变量。 after钩子在所有测试完成运行后运行一次。 它们可用于清除或重置测试之间共享的状态。 afterEach钩子在每次测试完成后但在下一次启动之前运行。 它可以用来清理或重置特定于测试的共享状态。

活动 4:使用测试环境

您的任务是升级您的斐波那契序列测试代码以使用 Mocha 测试框架。 使用斐波那契序列代码并测试您为活动 1:实现生成器创建的代码,并升级它以使用 Mocha 测试框架来测试代码。 您应该为n=0条件编写测试,并实现它,然后为测试编写测试,并实现n=1条件。 n=5n=6n=8重复上述步骤。 如果it()测试通过,则不带参数调用 done 回调函数,否则使用错误或其他truthy值调用 test done 回调函数。

要使用 Mocha 测试框架编写和运行测试,请执行以下步骤:

  1. 建立 NPM 项目并安装 mocha 模块。
  2. 向运行摩卡和test.js中的测试的package.json添加一个测试脚本。
  3. 创建一个具有斐波那契数列计算器功能的index.js文件。 出口这个函数。
  4. 创建test.js,使用摩卡框架测试斐波那契序列函数。 测试 n=0, n=1, n=2, n=5, n=7, n=9。

输出

Figure 4.7: Testing the Fibonacci sequence with Mocha

图 4.7:用摩卡测试斐波那契序列

您已经成功地利用了 Mocha 测试框架来编写和运行测试。

请注意

这个活动的解决方案可以在 288 页找到。

代码测试是开发人员可以拥有的最重要的技能之一。 测试代码就像去健身房。你知道这对你有好处,但通常很难开始。 在本章中,我们讨论了测试代码的原因、几种类型的代码测试以及几种 JavaScript 代码测试框架。 需要进行代码测试以确保程序的正确性。 测试驱动开发是开始将测试集成到项目中的最简单的方法之一。 TDD 的核心是在编写任何实现代码之前,编写测试,概述添加的任何特性或功能的需求。 有许多形式的代码测试。 在本章中,我们涵盖了单元测试、功能测试和集成测试。 这些类型的代码测试是最常见的,通常由两种方法之一构建:黑盒和白盒。 功能测试、单元测试和集成测试都可以在上一主题所述的许多框架中构建。

在下一章中,我们将介绍函数式编程的编码原则,并定义面向对象编程和函数式编程。