六、代码质量

学习目标

在本章结束时,你将能够:

  • 确定编写干净 JavaScript 代码的最佳实践
  • 执行检测并向节点项目添加一个 lint 命令
  • 在代码上使用单元、集成和端到端测试方法
  • 使用 Git 挂钩自动化检测和测试

在本章中,我们将重点讨论如何提高代码质量,设置测试,以及在 Git 提交之前自动运行测试。 这些技术可以用来确保错误在早期被发现,而不会在生产中出现。

简介

在前一章中,我们探讨了模块化设计的概念、ES6 模块以及它们在 Node.js 中的使用。 我们使用 Babel 将编译好的 ES6 JavaScript 转换成兼容的脚本。

在本章中,我们将讨论代码质量,这是专业 JavaScript 开发的关键品质之一。 当我们开始编写代码时,我们倾向于专注于解决简单的问题并评估结果。 当涉及到大多数开发人员开始的小型项目时,几乎不需要与他人沟通或作为大型团队的一部分工作。

随着项目的扩大,代码质量的重要性也会增加。 除了确保代码工作之外,我们还必须考虑其他开发人员,他们将使用我们创建的组件或更新我们编写的代码。

高质量的代码有几个方面。 首先也是最明显的一点是,它能做它想做的事情。 说起来容易做起来难。 通常,很难满足大型项目的需求。 更复杂的是,添加新特性常常会导致应用的某些现有部分出现错误。 我们可以通过良好的设计来减少这些错误,但即便如此,这些类型的破坏还是会发生。

随着敏捷开发变得越来越流行,代码更改的速度也在增加。 因此,测试比以往任何时候都更重要。 我们将演示如何使用单元测试来确认函数和类的正确功能。 除了单元测试之外,我们还将研究集成测试,它确保程序的所有方面都能按照预期正确地一起工作。

代码质量的第二个组成部分是性能。 我们代码中的算法可能会产生期望的结果,但它们能有效地做到吗? 我们将了解如何测试函数的性能,以确保算法在处理大量输入时能够在可接受的时间内返回结果。 例如,您可能有一个排序算法,它可以很好地处理 10 行数据,但当您尝试处理 100 行数据时,需要几分钟。

我们将在本章讨论的代码质量的第三个方面是可读性。 可读性是衡量人类阅读和理解代码的容易程度的指标。 您是否看过使用模糊函数和变量名编写的代码,或者使用容易引起误解的变量名编写的代码? 在编写代码时,要考虑到其他人可能必须阅读或修改它。 遵循一些基本的指导方针可以帮助提高可读性。

清晰命名

使代码更具可读性的最简单方法之一是清晰地命名。 使变量和函数的使用尽可能明显。 即使在一个人的项目中,在 6 个月后回到自己的代码时也很容易忘记每个函数的功能。 当你阅读别人的代码时,这是千真万确的。

确保你的名字清晰可读。 考虑下面的例子,一个开发人员创建了一个返回yymm格式的日期的函数:

function yymm() {
  let date = new Date();
  Return date.getFullYear() + "/" + date.getMonth();
}

当我们知道了上下文和这个函数的作用,这就很明显了。 但是对于第一次浏览代码的外部开发人员来说,yymm很容易造成一些困惑。

模糊函数应该以一种使其用法显而易见的方式进行重命名:

function getYearAndMonth() {
  let date = new Date();
  return date.getFullYear() + "/" + date.getMonth();
}

当使用正确的函数和变量命名时,就可以很容易地编写易读的代码。 考虑另一个例子,如果是晚上,我们想要开灯:

if(time>1600 || time<600) {
  light.state = true;
}

完全不清楚前面的代码中发生了什么。 1600600到底是什么意思?如果光的状态是true又意味着什么? 现在考虑将相同的函数重写如下:

if(time.isNight) {
  light.turnOn;
}

前面的代码说明了相同的过程。 不是问时间是否在 600 到 1600 之间,而是简单地问是不是晚上,如果是,我们就把灯打开。

除了可读性更强,我们还将夜间时间的定义放在了一个中心位置,isNight。 如果我们想让夜晚结束在 5:00 而不是 6:00,我们只需要更改isNight中的一行,而不是在我们的代码中找到time<600的所有实例。

公约

当谈到如何格式化或编写代码的约定时,有两类:行业或语言范围的约定和公司/组织范围的约定。 特定于行业或语言的约定通常被使用一种语言的大多数程序员所接受。 例如,在 JavaScript 中,一个全行业的约定是变量名使用驼峰大小写。

对于行业范围的约定很好的资源包括 W3 JavaScript 风格指南和 Mozilla MDN Web 文档。

除了行业范围的约定之外,软件开发团队或项目通常还会有一组进一步的约定。 有时,这些约定被编译成样式指南文档; 在其他情况下,这些约定是没有记录的。

如果您所在的团队有一个相对较大的代码库,那么记录特定的样式选择可能会很有用。 这将帮助您考虑哪些方面想要保持和执行新的更新,以及哪些方面可能想要更改。 它还可以帮助熟悉 JavaScript 但不熟悉公司细节的新员工适应新环境。

谷歌 JavaScript styleguide(https://google.github.io/styleguide/jsguide.html)是公司特有风格指南的一个很好的例子。 它包含一些一般有用的信息。 例如,2.3.3节讨论了在代码中使用非 ascii 码。 建议如下:

const units = 'μs';

比使用类似的东西更可取:

const units = '\u03bcs'; // 'μs'

使用没有评论的\u03bcs会更糟糕。 代码的含义越明显越好。

公司通常有一组库,它们喜欢用来做日志记录、处理时间值(例如 Moment.js 库)和测试。 这对于兼容性和代码重用非常有用。 有多个做类似事情的依赖项,由不同的开发人员使用,会增加编译项目的规模,例如,如果一个项目已经在使用 Bunyan 进行日志记录,而其他人决定安装一个替代库,如 Morgan。

注意:风格指南

花点时间阅读一些比较流行的 JavaScript 风格指南是值得的。 不要觉得有义务去遵循每一条规则或建议,但要习惯为什么要创建和执行规则背后的思考。 以下是一些值得一看的热门指南:

MSDN 风格指南:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide

固执己见 vs .不固执己见

说到惯例,你很可能会遇到“固执己见”这个词。 在研究现有的库和框架时,您经常会看到“固执己见的框架”这样的短语。 在这方面,“固执己见”指的是一项公约执行的严格程度:

固执己见的:严格执行自己选择的惯例和做事方法

非固执己见:不强制约定,即只要代码有效,它就可以使用

林特

检测是一个自动化的过程,在这个过程中,代码根据风格准则的标准进行检查和验证。 例如,设置了 linting 以确保两个空格而不是制表符的项目将检测制表符实例并提示开发人员进行更改。

了解检测是很重要的,但它对您的项目不是严格的要求。 当我在一个项目中工作时,当我决定是否需要进行筛选时,我考虑的主要要点是项目的规模和参与项目的团队的规模。

在中型到大型团队的长期项目中,检测确实很有用。 通常,新加入项目的人都有使用其他样式约定的经验。 这意味着您开始在文件之间,甚至在同一个文件中获得混合样式。 这导致项目变得缺乏组织性和可读性。

另一方面,如果你正在为黑客马拉松编写原型,我会建议你跳过测试。 它会增加项目的开销,除非您使用的是样板项目作为您的起点,而样板项目是与您首选的检测一起安装的。

还有一个风险是,筛选系统过于严格,最终会减慢开发速度。

好的筛选应该考虑项目,并在加强通用风格和不太严格之间找到平衡。

练习 29:设置 ESLint 和 Prettier 来监控代码中的错误

在这个练习中,我们将安装和设置 ESLint 和 Prettier 来监控代码的样式和语法错误。 我们将使用一个流行的 ESLint 约定,它是由 Airbnb 开发的,在某种程度上已经成为一种标准。

请注意

这个练习的代码文件可以在https://github.com/TrainingByPackt/Professional-JavaScript/tree/master/Lesson06/Exercise29/result中找到。

执行以下步骤来完成练习:

  1. Create a new folder and initialize an npm project:

    js mkdir Exercise29

    js cd Exercise29

    js npm init -y

    js npm install --save-dev eslint prettier eslint-config-airbnb-base eslint-config-prettier eslint-plugin-jest eslint-plugin-import

    我们在这里安装了几个开发人员依赖项。 除了eslintprettier,我们还安装了一个由 Airbnb 创建的初始配置,一个与 Prettier 一起工作的配置,以及一个为基于 jest 的测试文件添加样式异常的扩展。

  2. 创建一个.eslintrc文件:

    js {

    js "extends": ["airbnb-base", "prettier"],

    js "parserOptions": {

    【4】【5】

    js },

    js "env": {

    【显示】

    js    "node": true,

    js    "es6": true,

    js    "mocha": true,

    【病人】

    js },

    js "plugins": [],

    js "rules": {

    【t16.1】

    js     "error",

    js     {

    js       "vars": "local",

    js       "args": "none"

    js     }

    js    ],

    js    "no-plusplus": "off",

    js }

    【汽车】 3. 创建一个.prettierignore文件(类似于.gitignore文件,它只是列出了应该被 Prettier 忽略的文件)。 您的.prettierignore文件应该包含以下内容:

    js node_modules

    js build

    js dist

  3. 创建一个src文件夹,并在其中创建一个名为square.js的文件,该文件包含以下代码。 请确保包含 out- place 标签:

    js var square = x => x * x;

    js console.log(square(5));

  4. 在 npmpackage.json文件中创建lint脚本:

    js "scripts": {

    js    "lint": "prettier --write src/**/*.js"

    js },

  5. 接下来,我们将从命令行运行我们的新脚本,测试和演示prettier --write:

    js npm run lint

  6. Open src/square.js in a text editor; you can see that the out-of-place tab was removed:

    Figure 6.1: The out-of-place tab was removed

    图 6.1:错位选项卡被删除
  7. 接下来,回到package.json,扩展我们的 lint 脚本,在prettier完成后运行eslint:

    js "scripts": {

    js    "lint": "prettier --write src/**/*.js && eslint src/*.js"

    js },

  8. In the command line, run npm run lint again. You will encounter a linting error due to the code format in square.js:

    ```js

    prettier --write src//.js && eslint src/.js ```

    js src/square.js 49ms

    js /home/philip/packt/lesson_6/lint/src/square.js

    js 1:1 error   Unexpected var, use let or const instead no-var

    js 2:1 warning Unexpected console statement          no-console

    js 2 problems (1 error, 1 warning)

    js 1 error and 0 warnings potentially fixable with the --fix option.

    前面的脚本生成一个错误和一个警告。 该错误是由于当可以使用letconst时,使用了var。 不过,在这种特殊情况下,应该使用const,因为square的值没有被重新分配。 警告是关于我们对console.log的使用,它通常不应该在产品代码中发布,因为当错误发生时,它会使调试控制台输出变得困难。

  9. Open src/example.js and change var to const on line 1, as shown in the following figure:

    Figure 6.2: The var statement replaced to const

    图 6.2:var 语句被替换为 const
  10. 现在再运行npm run lint。 你现在只会收到以下警告:

    ```js

    prettier --write src//.js && eslint src/.js ```

    js src/js.js 48ms

    js /home/philip/packt/lesson_6/lint/src/js.js

    js 2:1 warning Unexpected console statement no-console

    js 1 problem (0 errors, 1 warning)

在这个练习中,我们安装并设置了 Prettier 用于自动代码格式化,并使用 ESLint 检查代码中常见的错误做法。

单元测试

单元测试是一种自动化的软件测试,它检查某些软件中的单个方面或功能是否按预期工作。 例如,一个计算器应用可能被分割成处理应用的图形用户界面(GUI)的函数和负责每种数学计算类型的另一组函数。

在这样的计算器中,可以设置单元测试以确保每个数学函数按预期工作。 这种设置允许我们快速查找任何由更改引起的不一致的结果或损坏的函数。 例如,这样一个计算器的测试文件可能包括以下内容:

test('Check that 5 plus 7 is 12', () => {
  expect(math.add(5, 7)).toBe(12);
});
test('Check that 10 minus 3 is 7', () => {
  expect(math.subtract(10, 3)).toBe(7);
});
test('Check that 5 multiplied by 3 is 15', () => {
  expect(math.multiply(5, 3).toBe(15);
});
test('Check that 100 divided by 5 is 20', () => {
  expect(math.multiply(100, 5).toBe(20);
});
test('Check that square of 5 is 25', () => {
  expect(math.square(5)).toBe(25);
});

每次更改代码库并签入版本控制时,前面的测试都会运行。 通常,当在多个地方使用的函数被更新并引起连锁反应,破坏其他函数时,会意外地出现错误。 如果发生了这样的更改,并且前面的一个语句变成了 false(例如,5 乘以 3 返回 16 而不是 15),我们将能够立即将新的代码更改与 break 关联起来。

这是一种非常强大的技术,可以在已经设置了测试的环境中理所当然地使用。 在没有这样一个系统的工作环境中,来自开发人员的更改或软件依赖项的更新可能会意外地破坏现有的功能,并提交给源代码控制。 后来,发现了 bug,就很难将坏掉的函数和引起它的代码更改联系起来。

同样重要的是要记住,单元测试确保的是某些工作子单元的功能,而不是整个项目的功能(其中多个功能一起工作以产生结果)。 这就是集成测试发挥作用的地方。 我们将在本章的后面探讨集成测试。

练习 30:设置 Jest 测试来测试计算器应用

在这个练习中,我们将演示如何使用 Jest 设置单元测试,Jest 是 JavaScript 生态系统中最流行的测试框架。 我们将继续我们的计算器应用示例,并设置一个函数的自动测试,该函数取一个数并输出其平方。

请注意

这个练习的代码文件可以在https://github.com/TrainingByPackt/Professional-JavaScript/tree/master/Lesson06/Exercise30中找到。

执行以下步骤来完成练习:

  1. 在命令行中,导航到Exercise30/start练习文件夹。 这个文件夹包括一个src文件夹,其中包含我们将要运行测试的代码。
  2. 初始化一个npm项目,输入以下命令:
  3. 通过输入以下命令,使用--save-dev标志(这表明依赖项是开发所需的,而不是生产所需的)安装 Jest:
  4. 创建一个名为__tests__的文件夹。 这是 Jest 查找测试的默认位置:
  5. 现在我们要在__tests__/math.test.js中创建第一个测试。 导入src/math.js,并确保math.square(5)运行返回25:

    js const math = require('./../src/math.js');

    js test('Check that square of 5 is 25', () => {

    js expect(math.square(5)).toBe(25);

    js });

  6. Open package.json and modify the test script so that it runs jest. Notice the scripts section in the following screenshot:

    Figure 6.3: The test script modified so that it runs Jest

    图 6.3:修改测试脚本以运行 Jest
  7. On the command line, enter the npm run test. This should return a message that tells us the wrong value was found, as shown in the following code:

    js FAIL __test__/math.test.js

    js ✕ Check that square of 5 is 25 (17ms)

    js ● Check that square of 5 is 25

    js    expect(received).toBe(expected) // Object.is equality

    js    Expected: 25

    js    Received: 10

    js     2 |

    js     3 | test('Check that square of 5 is 25', () => {

    js    > 4 |  expect(math.square(5)).toBe(25);

    js       |                  ^

    js     5 | });

    js     6 |

    js     at Object.toBe (__test__/math.test.js:4:26)

    js Test Suites: 1 failed, 1 total

    js Tests:     1 failed, 1 total

    js Snapshots:  0 total

    js Time:      1.263s

    这个错误触发是因为开始代码故意在square函数中包含了一个错误。 我们不是将数字相乘,而是将数值加倍。 注意,收到的答案的数量是10

  8. 通过打开文件并修复square函数来修复错误。 它应该将x相乘,如下面的代码所示,而不是将其加倍:

  9. 我们的代码修复后,让我们再次用npm run test进行测试。 你会得到一条成功信息,如下所示:

Figure 6.4: Success message shown after testing with npm run test

图 6.4:使用 npm run test 测试后显示的成功消息

在这个练习中,我们设置了一个 Jest 测试,以确保输入为 5 的square函数运行时返回 25。 我们还通过运行带有错误代码(返回 10 而不是 25)的测试来查看返回错误值时的预期结果。

集成测试

因此,我们已经讨论了单元测试,它对于在项目代码更改时查找错误原因非常有用。 然而,也有可能项目通过了所有单元测试,但没有按预期工作。 这是因为整个项目包含额外的逻辑,将我们的功能和静态组件(如 HTML、数据和其他工件)结合在一起。

集成测试可用于确保项目从更高的层次工作。 例如,我们的单元测试直接调用math.square之类的函数,而集成测试将测试多个功能片段一起工作以获得特定的结果。

通常,这意味着将多个模块组合在一起,或者与数据库或其他外部组件或 api 进行交互。 当然,集成更多的部件意味着集成测试需要更长的时间,因此它们应该比单元测试更少地使用。 集成测试的另一个缺点是,当一个测试失败时,有多种可能的原因。 相反,失败的单元测试通常很容易修复,因为正在测试的代码位于指定的位置。

Exercise 31: Integration Testing with Jest

在这个练习中,我们将继续上次的 Jest 练习,在那个练习中,我们测试了square函数在响应 5 时返回 25。 在这个练习中,我们将继续添加一些新的测试,这些测试将我们的函数相互结合使用:

  1. 在命令行中,导航到Exercise31/start练习文件夹,使用npm:

    js npm install

    安装依赖项 2. 创建一个名为__tests__的文件夹:

    js mkdir __tests__

  2. 创建一个名为__tests__/math.test.js的文件。 然后,在顶部,导入math库:

    js const math = require('./../src/math.js');

  3. 与上一个练习类似,我们将添加一个测试。 然而,这里的主要区别是我们结合了多种功能:

    js test('check that square of result from 1 + 1 is 4', () => {

    js expect(math.square(math.add(1,1))).toBe(4);

    js });

  4. 在上述测试的基础上增加一个测量性能的定时器:

    js test('check that square of result from 1 + 1 is 4', () => {

    js const start = new Date();

    js expect(math.square(math.add(1,1))).toBe(4);

    js expect(new Date() - start).toBeLessThan(5000);

    js });

  5. 现在,通过运行npm test来测试以确保一切正常:

Figure 6.5: Running npm test to make sure everything is working fine

图 6.5:运行 npm 测试以确保一切正常运行

您应该会看到类似于上图的输出,每个测试都通过了一个预期的结果。

应该注意的是,这些集成测试有些简单。 在真实场景中,集成测试结合了我们前面演示过的功能,但它们来自不同的来源。 例如,当您有多个由不同团队创建的组件时,集成测试将确保所有组件都能协同工作。 通常,错误可能是由简单的事情引起的,例如更新外部库。

其思想是将应用的多个部分集成在一起,从而使您有更大的机会找到出错的地方。

代码性能 Fibonacci 示例

通常,一个问题有不止一个解决方案。 虽然所有解决方案可能返回相同的结果,但它们可能没有相同的性能。 例如,求第 n 个斐波那契数列的问题。 斐波那契数列是一种数学模式,其中序列中的下一个数是最后两个数(1,1,2,3,5,8,13,…)的和。

考虑下面的解决方案,其中 Fibonacci 递归地调用自己:

function fib(n) {
  return (n<=1) ? n : fib(n - 1) + fib(n - 2);
}

前面的例子表明,如果我们想递归地得到 Fibonacci 序列的第 n 个数字,那么就得到n- 1 的 Fibonacci 加上n- 2 的 Fibonacci,除非n是 1,在这种情况下,返回 1。 它可以工作,并将返回任何给定数字的正确答案。 然而,随着n值的增加,执行时间呈指数增长。

要了解它的执行速度有多慢,可以将fib函数添加到一个新文件中,并通过控制台记录如下结果来使用该函数:

console.log(fib(37));

接下来,在命令行上运行以下命令(time应该在大多数基于 Unix 和 mac 的环境中可用):

time node test.js

在一台特定的笔记本电脑上,我得到了如下结果,表明斐波那契的第 37 位数字是24157817,执行时间为 0.441 秒:

24157817
real 0m0.441s
user 0m0.438s
sys 0m0.004s

现在打开相同的文件,将37更改为44。 然后,再次运行相同的time node test命令。 在我的例子中,仅仅增加 7 就导致执行时间增加了 20 倍:

701408733
real 0m10.664s
user 0m10.653s
sys 0m0.012s

我们可以以更有效的方式重写相同的算法,以提高较大数字的速度:

function fibonacciIterator(a, b, n) {
  return n === 0 ? b : fibonacciIterator((a+b), a, (n-1));
}
function fibonacci(n) {
  return fibonacciIterator(1, 0, n);
}

尽管它看起来更复杂,但由于执行速度,这种生成斐波那契数的方法是优越的。

使用 Jest 进行测试的一个缺点是,在前面的场景中,无论是慢版本还是快版本的 Fibonacci 都将通过。 然而,在需要快速处理的现实应用中,慢版本显然是不可接受的。

要防止这种情况,您可能需要添加一些基于性能的测试,以确保功能在特定时间内完成。 下面是一个创建自定义计时器的示例,以确保函数在 5 秒内完成:

test('Timer - Slow way of getting Fibonacci of 44', () => {
  const start = new Date();
  expect(fastFib(44)).toBe(701408733);
  expect(new Date() - start).toBeLessThan(5000);
});

注意:Jest 的未来版本

手动向所有函数添加计时器可能有些麻烦。 出于这个原因,Jest 项目中正在讨论创建一种更简单的语法来完成前面所做的工作。

要查看与此语法相关的讨论,以及它是否已经解决,请在 GitHub 上查看 Jest 问题#6947https://github.com/facebook/jest/issues/6947

Exercise 32: ensure Performance with Jest

在这个练习中,我们将使用前面描述的技术来测试获取斐波那契的两种算法的性能:

  1. 在命令行中,导航到Exercise32/start练习文件夹,使用npm:

    js npm install

    安装依赖项。 2. 创建一个名为__tests__的文件夹:

    js mkdir __tests__

  2. 创建一个名为__tests__/fib.test.js的文件。 在顶部,导入快速和缓慢的斐波那契函数(这些已经在start文件夹中创建):

  3. 添加一个快速 Fibonacci 测试,它创建一个计时器,并确保计时器的运行时间不超过 5 秒:
  4. 接下来,添加一个慢速 Fibonacci 测试,它也检查运行时间小于 5 秒:
  5. 在命令行中,使用npm test命令运行测试:

Figure 6.6: Result from the Fibonacci tests

图 6.6:斐波那契测试的结果

注意前面提到计时器的部分的错误响应。 函数运行时间的预期结果小于 5,000 毫秒,但在我的例子中,我实际收到了 10,961 毫秒。 根据你电脑的速度,你可能会得到不同的结果。 如果您没有收到这个错误,可能是因为您的计算机速度太快,不到 5000 毫秒就完成了。 如果是这种情况,请尝试降低触发错误的预期最大时间。

端到端测试

集成测试结合了软件项目的多个单元或功能,端到端测试通过模拟软件的实际使用更进一步。

例如,当我们的单元测试直接调用诸如math.square之类的函数时,端到端测试将加载计算器的图形界面并模拟按下一个数字(比如 5),然后是正方形按钮。 几秒钟后,端到端测试将在图形界面中查看结果答案,并确保它等于预期的 25。

由于开销,端到端测试应该更少地使用,但它是测试过程中确保一切都按预期工作的重要的最后一步。 相反,单元测试运行起来相对较快,因此可以更频繁地运行,而不会减慢开发速度。 下图显示了推荐的测试分布:

Figure 6.7: Recommended distribution of tests

图 6.7:推荐的测试分布

注意:集成测试与端到端测试

应该注意的是,在集成测试和端到端测试之间可能存在一些重叠。 对测试类型的解释可能因公司而异。

传统上,测试被划分为单元测试或集成测试。 随着时间的推移,其他分类也变得流行起来,如系统、接受和端到端。 因此,特定测试的类型可能会有重叠。

木偶师

2018 年,谷歌发布了PuppeteerJavaScript 库,大大提高了在基于 JavaScript 的项目上设置端到端测试的便捷性。 Puppeteer 是 Chrome 浏览器的无头版本,这意味着它没有 GUI 组件。 这是至关重要的,因为这意味着我们正在用完整的 Chrome 浏览器测试我们的应用,而不是模拟。

可以通过类似 jquery 的语法来控制 Puppeteer,其中 HTML 页面上的元素可以通过 ID 或类来选择并与之交互。 例如,下面的代码打开谷歌 News,找到一个.rdp59b类,单击它,等待 3 秒,最后截图:

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://news.google.com');
  const more = await page.$(".rdp59b");
  more.click();
  await page.waitFor(3000);
  await page.screenshot({path: 'news.png'});
  await browser.close();
})();

请记住,在前面的示例中,我们选择了一个看起来像是自动生成的.rdp59b类; 因此,这个类在未来很可能会发生变化。 在类名改变的情况下,脚本将不再工作。

如果在阅读本文时,您发现前面的脚本不起作用,我建议您更新它。 一个最好的工具,当工作与木偶是 Chrome 开发工具。 我通常的工作流程是进入我要编写脚本的网站,右键单击我要瞄准的元素,如下图所示:

Figure 6.8: Right-click to inspect in Chrome

图 6.8:在 Chrome 中单击右键进行检查

一旦你点击Inspect,DOM 浏览器就会弹出,你将能够看到与元素相关的任何类或 id:

Figure 6.9: DOM explorer in Chrome DevTools

图 6.9:Chrome DevTools 中的 DOM 浏览器

注:用于 Web 抓取和自动化的木偶师

除了对编写端到端测试有用之外,Puppeteer 还可以用于 web 抓取和自动化。 几乎所有可以在普通浏览器中完成的事情都可以自动完成(只要有正确的代码)。

除了能够通过选择器选择页面上的元素,正如我们之前所看到的,Puppeteer 还能够完全访问键盘和鼠标模拟。 因此,更复杂的事情,如自动化网络游戏和日常任务是可能的。 有些人甚至可以通过它绕过验证码之类的东西。

练习 33:端到端木偶测试

在这个练习中,我们将使用 Puppeteer 手动打开一个基于 HTML/ javascript 的计算器,并像终端用户那样使用它。 我不想针对一个在线网站,因为它的内容经常变化或离线。 因此,我在项目文件的Exercise33/start中包含了一个 HTML 计算器。

你可以通过 npm 安装依赖项,运行npm start,然后在浏览器中进入localhost:8080来查看:

Figure 6.10: Site showing the demonstration of a calculator created using Puppeteer

图 6.10:站点显示了使用 Puppeteer 创建的计算器的演示

在这个练习中,我们将创建一个脚本,用于打开站点,按下按钮,然后检查站点是否有正确的结果。 我们不是仅仅检查函数的输出,而是列出要在站点上执行的操作,并指定 HTML 选择器作为运行测试的值。

执行以下步骤来完成练习:

  1. 打开Exercise33/start文件夹,安装现有的依赖项:

    js npm install

  2. 安装所需的jestpuppeteerjest-puppeteer包:

    js npm install --save-dev jest puppeteer jest-puppeteer

  3. 打开package.json并配置 Jest 使用jest-puppeteer预设,这将自动设置 Jest 与 Puppeteer:

    js "jest": {

    js    "preset": "jest-puppeteer"

    js },

  4. Create a file called jest-puppeteer.config.js and add the following to it:

    js module.exports = {

    js server: {

    js    command: 'npm start',

    js    port: 8080,

    js },

    js }

    上述配置将确保在测试阶段之前运行npm start命令。 它还告诉 Puppeteer 在port: 8080上寻找我们的 web 应用。

  5. 创建一个名为__tests__的新文件夹,就像我们在前面的例子中做的那样:

    js mkdir __test__

  6. Inside the __tests__ folder, create a file called test.test.js that contains the following:

    js describe('Calculator', () => {

    js beforeAll(async () => {

    js    await page.goto('http://localhost:8080')

    js })

    js it('Check that 5 times 5 is 25', async () => {

    js    const five = await page.$("#five");

    js    const multiply = await page.$("#multiply");

    js    const equals = await page.$("#equals");

    js    await five.click();

    js    await multiply.click();

    js    await five.click();

    js    await equals.click();

    js    const result = await page.$eval('#screen', e => e.innerText);

    js    expect(result).toMatch('25');

    js })

    js })

    前面的代码是一个完整的端到端测试,用于将 5 乘以 5 并确认接口内返回的答案是 25。 在这里,我们打开本地网站,按 5,按乘法,按 5,按等于,然后检查 ID 为screendiv的值。

  7. 使用npm运行测试:

Figure 6.11: Output after running the calculator script

图 6.11:运行计算器脚本后的输出

您应该看到如上面的图所示的结果,输出为 25。

Git Hooks

这里讨论的测试和检测命令对于维护和改进代码质量和功能非常有用。 然而,在实际开发的最激烈阶段,我们的重点是特定的问题和截止日期,很容易忘记运行 linting 和测试命令。

这个问题的一个流行解决方案是使用 Git 钩子。 Git 钩子是 Git 版本控制系统的一个特性。 Git 钩子指定在 Git 进程的某个特定点上运行的终端命令。 Git 钩子可以在提交之前运行; 之后,当用户通过拉更新; 以及许多其他特定的点。 完整的 Git 钩子列表可以在https://git-scm.com/docs/githooks中找到。

出于我们的目的,我们将只关注使用预提交钩子。 这将允许我们在提交代码到源代码之前找到任何格式问题。

注意:探索 Git

另一种探索 Git 钩子的有趣方法是打开任何 Git 版本控制项目,查看hooks文件夹。

默认情况下,任何新的.git项目都会在.git/hooks文件夹中包含一个很大的示例列表。 探索它们的内容,并通过以下模式重命名它们来触发它们:

<hook-name>.sample to <hook-name>

Exercise 34: Setting up a Local Git Hook .【T0

在这个练习中,我们将设置一个本地 Git 钩子,在允许使用 Git 提交之前运行lint命令:

  1. 在命令行中,导航到Exercise34/start练习文件夹并安装依赖项:

    js npm install

  2. 初始化文件夹为 Git 项目:

  3. 创建.git/hooks/pre-commit文件,文件内容如下:

    ```js

    !/bin/sh

    ```

    js npm run lint

  4. 如果在基于 OS X 或 linux 的系统上,通过运行以下命令使文件可执行(这在 Windows 上不是必需的):

  5. We'll now test the hook by making a commit:

    js git add package.json

    js git commit -m "testing git hook"

    下面是上述代码的输出:

    Figure 6.12: Git hook being run before committing to Git

    图 6.12:Git hook 在提交到 Git 之前正在运行

    在将代码提交到源代码之前,您应该看到正在运行的lint命令,如上面的截图所示。

  6. Next, let's test failure by adding some code that will generate a linting error. Modify your src/js.js file by adding the following line:

    js       let number = square(5);

    请确保将不必要的选项卡保留在前面的行中,因为这将触发 lint 错误。

  7. Repeat the process of adding the file and committing it:

    js git add src/js.js

    js git commit -m "testing bad lint"

    下面是上述代码的输出:

Figure 6.13: A failed linting before committing the code to git

图 6.13:将代码提交到 git 之前的失败检测

您应该看到lint命令像以前一样运行; 然而,在它运行之后,代码不会像上次那样提交,因为 Git 钩子会返回一个错误。

与 Husky 分享 Git hook

关于 Git 钩子,需要注意的一个重要因素是,因为这些钩子位于.git文件夹本身,所以它们不被认为是项目的一部分。 因此,它们不会共享给协作者使用的中央 Git 存储库。

然而,Git 钩子在协作项目中最有用,因为新开发人员可能不完全了解项目的约定。 当一个新开发人员克隆一个项目,做出一些更改,尝试提交,并立即根据测试得到反馈时,这是一个非常方便的过程。

husky节点库就是这样创建的。 它允许您使用一个名为.huskyrc的配置文件来跟踪源代码中的 Git 钩子。 当一个新开发人员安装一个项目时,钩子将是活动的,而开发人员不需要做任何事情。

练习 35:用 Husky 设置一个 Commit Hook

在这个练习中,我们将设置一个 Git 钩子,它的功能与练习 34,设置一个本地 Git 钩子,中的功能相同,但它的优点是可以在团队中共享。 通过使用husky库而不是直接git库,我们可以确保任何克隆项目的人在提交任何更改之前也有运行lint的钩子:

  1. 在命令行中,导航到Exercise35/start练习文件夹并安装依赖项:

    js npm install

  2. Create a file called .huskyrc that contains the following:

    js {

    js "hooks": {

    js    "pre-commit": "npm run lint"

    js }

    js }

    前面的文件是本练习中最重要的部分,因为它准确地定义了在 Git 进程中的什么点上运行什么命令。 在我们的例子中,我们在将任何代码提交到源代码之前运行lint命令。

  3. 使用git init:

    js git init

    命令将文件夹初始化为 Git 项目 4. 使用npm:

    js npm install --save-dev husky

    安装 Husky 5. Make a change to src/js.js that will be used for our test commit. As an example, I'll add a comment as follows:

    Figure 6.14: The test commit comment

    图 6.14:测试提交注释
  4. Now, we'll run a test ensuring it works like in the previous example:

    js git add src/js.js

    js git commit -m "test husky hook"

    下面是上述代码的输出:

    Figure 6.15: Output after committing the test husky hook

图 6.15:提交 test husky 钩子后的输出

我们收到了一个关于我们使用console.log的警告,但是为了我们的目的,你可以忽略它。 重点是我们已经使用 Husky 设置了 Git 钩子,所以其他安装项目的人也会设置这些钩子,而不是直接在 Git 中设置。

注意:初始化沙哑的

请注意,npm install --save-dev husky是在创建 Git 存储库之后运行的。 当您安装 Husky 时,它会运行所需的命令来设置 Git 钩子。 但是,如果项目不是 Git 存储库,那么它就不能这样做。

如果您有任何与此相关的问题,在初始化 Git 存储库之后,请尝试重新运行npm install --save-dev husky

Exercise 36: Getting Elements by Text with Puppeteer

在这个练习中,我们将编写一个 Puppeteer 测试来验证一个小测试应用是否工作。 如果你进入练习文件夹并找到练习 36的起点,你可以运行npm start来查看我们将要测试的小测验:

Figure 6.16: Puppeteer showing a small quiz app

图 6.16:Puppeteer 展示了一个小测试应用

在本应用中,点击问题的正确答案,问题消失,分数增加 1:

  1. 在命令行中,导航到Exercise36/start练习文件夹并安装依赖项:

    js npm install --save-dev jest puppeteer jest-puppeteer

  2. 通过修改scripts部分,向package.json添加一个测试脚本,使其看起来如下所示:

  3. 添加一个 Jest 部分到package.json,告诉 Jest 使用木偶预设:
  4. js module.exports = {

    js server: {

    js    command: 'npm start',

    js    port: 8080,

    js },

    js }

    js },

    js }

  5. 创建一个名为__test__的文件夹,我们将在其中放置 Jest 测试:

    js mkdir __test__

  6. 在文件夹quiz.test.js中创建一个测试。

    js describe('Quiz', () => {

    js beforeAll(async () => {

    js    await page.goto('http://localhost:8080')

    js })

    js // tests will go here

    js })

  7. Next, replace the comment in the preceding code with a test for the first question in our quiz:

    js it('Check question #1', async () => {

    js    const q1 = await page.$("#q1");

    js    let rightAnswer = await q1.$x("//button[contains(text(), '10')]");

    js    await rightAnswer[0].click();

    js    const result = await page.$eval('#score', e => e.innerText);

    js    expect(result).toMatch('1');

    js })

    注意我们对q1.$x("//button[contains(text(), '10')]")的使用。 我们不是使用 ID,而是搜索包含文本10的按钮的答案。 当解析一个不使用需要交互的元素的 id 的网站时,这非常有用。

  8. The following test is added to the last step. We'll add three new tests, one for each question:

    js it('Check question #2', async () => {

    js    const q2 = await page.$("#q2");

    js    let rightAnswer = await q2.$x("//button[contains(text(), '36')]");

    js    await rightAnswer[0].click();

    js    const result = await page.$eval('#score', e => e.innerText);

    js    expect(result).toMatch('2');

    js })

    js it('Check question #3', async () => {

    js    const q3 = await page.$("#q3");

    js    let rightAnswer = await q3.$x("//button[contains(text(), '9')]");

    js    await rightAnswer[0].click();

    js    const result = await page.$eval('#score', e => e.innerText);

    js    expect(result).toMatch('3');

    js })

    js it('Check question #4', async () => {

    js    const q4 = await page.$("#q4");

    js    let rightAnswer = await q4.$x("//button[contains(text(), '99')]");

    js    await rightAnswer[0].click();

    js    const result = await page.$eval('#score', e => e.innerText);

    js    expect(result).toMatch('4');

    js })

    注意,在每个测试的底部有一个预期结果,比最后一个高一个; 这是我们在页面上追踪的比分。 如果一切正常,第四次测试将得到 4 分。

  9. Finally, return to the command line so that we can confirm the correct results. Run the test command as follows:

    js npm test

    下面是上述代码的输出:

Figure 6.17: Command line confirming the correct results

图 6.17:确认正确结果的命令行

如果您正确地完成了所有操作,您应该看到四个通过的测试作为运行npm test的响应。

活动 7:put It All Together

在这个活动中,我们将结合本章的几个方面。 从一个使用 HTML/JavaScript 的预构建计算器开始,你的任务如下:

  • 创建一个lint命令,使用eslint-config-airbnb-base包根据prettiereslint检查项目,正如在前面的练习中所做的那样。
  • 使用jest安装puppeteer,并在package.json中创建一个test命令,运行jest
  • 创建一个木偶测试,使用计算器计算 777 乘 777,并确保返回的答案是 603,729。
  • 创建另一个木偶测试,计算 3.14 除以 2,并确保返回的答案是 1.57。
  • 在使用 Git 提交之前,安装并设置 Husky 以运行 linting 和 testing 命令。

执行以下翼步骤来完成活动(高级步骤):

  1. 安装测试练习中列出的开发人员依赖项(eslintprettiereslint-config-airbnb-baseeslint-config-prettiereslint-plugin-jesteslint-plugin-import)。
  2. 添加eslint配置文件.eslintrc
  3. 添加一个。 prettierignore文件。
  4. 添加一个lint命令到您的package.json文件。
  5. 打开assignment文件夹,安装与 Jest 一起使用 Puppeteer 的开发依赖项。
  6. 修改您的package.json文件,添加一个选项告诉 Jest 使用jest-puppeteer预设。
  7. 向运行jestpackage.json添加test脚本。
  8. 创建jest-puppeteer.config.js来配置 Puppeteer。
  9. __tests__/calculator.js处创建一个测试文件。
  10. 创建一个 Husky 文件在.huskyrc
  11. 通过运行npm install --save-dev husky安装husky作为开发依赖项。

预期输出

Figure 6.18: The final output showing calc.test passed

图 6.18:显示 calc.test 通过的最终输出

完成任务后,您应该能够运行npm run``lint命令和npm test命令,并让测试通过,如前面的截图所示。

请注意

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

小结

在本章中,我们关注了代码质量的各个方面,重点放在自动化测试上。 我们从清晰命名的基础开始,并熟悉该语言的行业范围约定。 通过遵循这些约定并清晰地编写代码,我们能够使代码更具可读性和可重用性。

在此基础上,我们研究了如何使用一些流行的工具(包括 Prettier、ESLint、Jest、Puppeteer 和 Husky)在 Node.js 中创建检测和测试命令。

除了设置测试,我们还讨论了类别测试及其用例。 我们进行了单元测试,以确保单个功能按照预期工作,并进行了集成测试,以组合程序的多个功能或方面,以确保事情能够协同工作。 然后,我们执行端到端测试,这将打开应用的界面,并像终端用户那样与之交互。

最后,我们讨论了如何通过使用 Git 钩子自动运行检测和测试脚本来将它们结合在一起。

在下一章中,我们将研究构造函数、承诺和 async/await。 我们将使用这些技术以一种现代的方式重构 JavaScript,利用 ES6 中的新特性。