用 Jest 进行 JavaScript 测试(2019)

14
作者:Valentino Gagliardi

翻译:疯狂的技术宅

原文:https://www.valentinog.com/bl...

未经允许严禁转载

测试是什么意思?

在技​​术术语中测试意味着检查我们的代码是否符合某些预期。例如:给定一些输入,一个名为“transformer”的函数应返回预期的输出

有许多类型的测试,很快你就会被术语所淹没,让我们长话短书。测试分为三大类

  • 单元测试
  • 集成测试
  • UI测试

在这个 Jest 教程中,我们将仅涵盖单元测试,但在文章的最后,你将找到更多用于其他类型测试的资源。

什么是Jest?

Jest 是一个 JavaScript 测试运行器,即用于创建、运行结构化测试的 JavaScript 库。 Jest 作为 NPM 包发布,你可以将其安装在任何 JavaScript 项目中。 Jest 是目前最受欢迎的测试运行器之一,也是 Create React App 的默认选择。

首先要做的事情:我怎么知道要测试些什么?

当谈到测试时,即使是简单的代码块也会使初学者瘫痪。最常见的问题是“我怎么知道要测试些什么?”。如果你正在编写 Web 应用,那么一个好的起点就是测试应用的每个页面和每个用户交互。但 Web 应用也由单元代码组成,如函数和模块,也需要进行测试。很多时候有两种情况:

  • 你维护没有测试祖传代码
  • 你必须凭空实现新功能

该怎么办?对于这两种情况,你可以通过考虑代码来检查,以检查给定函数是否产生预期结果**。以下是典型测试流程的样子:

应该怎么办? 对于这两种情况,你可以通过将测试看作检查给定函数是否产生预期结果的代码来帮助自己。 以下是典型测试流程的样子:

  1. 导入要测试的函数
  2. 给函数输入
  3. 定义期望输出
  4. 检查函数是否按照预期输出

就是这样。如果你按照这些术语思考,测试不再可怕:输入 - 预期输出 - 断言结果。接下来我们还会看到一个方便的工具,用于检查几乎确切的测试内容。现在就动手学习 Jest!

设置项目

与每个 JavaScript 项目一样,你需要一个 NPM 环境(确保在你的系统上安装了 Node)。创建一个新文件夹并用以下命令初始化项目:

mkdir getting-started-with-jest && cd $_
npm init -y

接下来安装Jest:

npm i jest --save-dev

我们还需要用配置一个 NPM 脚本,用于从命令行运行我们的测试。打开 package.json 并配置名为“test”的脚本以运行Jest:

  "scripts": {
    "test": "jest"
  },

规范和测试驱动开发

作为开发者,我们都喜欢创意自由。但是当谈到严肃的事情时,大部分时间你都没有那么多的特权。通常我们必须遵循规范,即建立的书面或口头描述

在本教程中,我们从项目经理那里得到了一个相当简单的规范。一个超级重要的客户端需要一个函数来过滤一个对象数组。

对于每个对象,我们必须检查名为“url”的属性,如果属性的值与给定的术语匹配,那么我们应该在结果数组中包含匹配的对象。作为一个精通测试的 JavaScript 开发人员,你想要遵循测试驱动开发,这是一个强制在开始编码之前编写失败测试的学科。

默认情况下,Jest 希望在项目下名为 tests 的文件夹中找到测试文件。创建新文件夹:

cd getting-started-with-jest
mkdir __tests__

接下来在 tests 中创建一个名为 filterByTerm.spec.js 的新文件。你可能想知道为什么扩展名是“.spec。”。这是一个借用 Ruby 的约定,用于将文件标记为给定功能的规范

现在来测试吧!

测试结构和第一次失败的测试

现在创建你的第一次Jest测试。打开 filterByTerm.spec.js 并创建一个测试块:

describe("Filter function", () => {
  // test stuff
});

我们的第一个朋友是 describe,一个用于包含一个或多个相关测试的 Jest 方法。每次开始为功能编写一套新测试时,都会将其包含在 describe 块中。正如你所看到的,它需要两个参数:一个用于描述测试套件的字符串,还有一个用于包装实际测试的回调函数。

接下来我们将遇到另一个名为 test 的函数,它是实际的测试块:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    // actual test
  });
});

这时我们已准备好编写测试了。请记住,测试是关于输入、功能和预期输出的问题。首先定义一个简单的输入,一个对象数组:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
  });
});

接下来定义预期结果。根据规范,测试中的函数应该省略其 url 属性与给定搜索项不匹配的对象。我们可以期待例如具有单个对象的数组,给定 “link” 作为搜索项:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
  });
});

现在准备编写实际的测试。我们将使用 expect 和一个 Jest matcher 来检查这个函数在调用时返回的预期结果。这是测试:

expect(filterByTerm(input, "link")).toEqual(output);

为了进一步细分,你可以在代码中调用函数:

filterByTerm(inputArr, "link");

在 Jest 测试中,你应该将函数调用包含在 expect 中,它与匹配器(用于检查输出的Jest函数)一起进行实际测试。这是完整的测试:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
  });
});

(有关 Jest 匹配器的更多信息,请查看文档(https://jestjs.io/docs/en/get...))。

你可以这样执行测试:

npm test

你会看到测试失败了:

FAIL  __tests__/filterByTerm.spec.js
  Filter function
    ✕ it should filter by a search term (2ms)
  ● Filter function › it should filter by a search term (link)
    ReferenceError: filterByTerm is not defined
       9 |     const output = [{ id: 3, url: "https://www.link3.dev" }];
      10 | 
    > 11 |     expect(filterByTerm(input, "link")).toEqual(output);
         |     ^
      12 |   });
      13 | });
      14 |

“ReferenceError: filterByTerm is not defined”. 实际上这是一件好事。我们会在下一节修复它!

修复测试

真正缺少的是 filterByTerm 的实现。为方便起见,我们将在测试所在的同一文件中创建该函数。在一个实际项目中,你需要在另一个文件中定义该函数并从测试文件中导入它

为了进行测试,我们将使用一个名为 filter 的原生 JavaScript 函数,它可以过滤掉数组中的元素。这是 filterByTerm 的最小实现:

function filterByTerm(inputArr, searchTerm) {
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(searchTerm);
  });
}

以下是它的工作原理:对于输入数组的每个元素,我们检查“url”属性,使用 match 方法将其与正则表达式进行匹配。这是完整的代码:

function filterByTerm(inputArr, searchTerm) {
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(searchTerm);
  });
}
describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
  });
});

再次运行测试:

npm test

看到它通过了!

 PASS  __tests__/filterByTerm.spec.js
  Filter function
    ✓ it should filter by a search term (link) (4ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.836s, estimated 1s

很好。但我们完成了测试吗?还没有。 使我们的函数失败需要什么条件?让我们用大写搜索词强调函数:

function filterByTerm(inputArr, searchTerm) {
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(searchTerm);
  });
}
describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
    expect(filterByTerm(input, "LINK")).toEqual(output); // New test
  });
});

运行测试......它将失败。该再次修复它了!

Jest Tutorial: fixing the test for uppercase

Jest Tutorial:修复大写测试

filterByTerm 也应该考虑大写的搜索术语。换句话说,即使搜索项是大写字符串,它也应该返回匹配的对象:

filterByTerm(inputArr, "link");
filterByTerm(inputArr, "LINK");

为了测试这种情况,我们引入了一个新测试:

expect(filterByTerm(input, "LINK")).toEqual(output); // New test

为了使它通过,我们可以调整提供给 match 的正则表达式:

//
    return arrayElement.url.match(searchTerm);
//

我们可以构建一个不区分大小写的正则表达式,而不是直接传递 searchTerm,也就是说,无论怎样的字符串都匹配的表达式。这是修复:

function filterByTerm(inputArr, searchTerm) {
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

这是完整的测试:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
    expect(filterByTerm(input, "LINK")).toEqual(output);
  });
});
function filterByTerm(inputArr, searchTerm) {
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

再次运行并看到它通过。做得好!作为练习,你要写两个新的测试并检查以下条件:

  1. 测试搜索词“uRl”
  2. 测试空搜索词。该函数应如何处理?

你将如何构建这些新测试?

在下一节中,我们将看到测试的另一个重要主题:代码覆盖率

代码覆盖率

什么是代码覆盖率?在谈论它之前,先让我们快速调整一下代码。在项目根目录中创建一个名为 src 的新文件夹,并创建一个名为 filterByTerm.js 的文件,放置并导出我们的函数:

mkdir src && cd _$
touch filterByTerm.js

这是文件 filterByTerm.js

function filterByTerm(inputArr, searchTerm) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
module.exports = filterByTerm;

现在假装我是你的新同事。我对测试一无所知,我应该直接在该函数内部添加一个新的 if语句,而不是要求更多的上下文:

function filterByTerm(inputArr, searchTerm) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!inputArr.length) throw Error("inputArr cannot be empty"); // new line
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
module.exports = filterByTerm;

filterByTerm 中有一行新的代码,似乎不会被测试。除非我告诉你“有一个新的测试声明”你不会在我们的函数中确切地知道测试。几乎不可能想象我们的代码可以采用的所有路径,因此需要一种有助于揭示这些盲点的工具

该工具被称为代码覆盖,它是工具箱中的强大工具。 Jest 具有内置代码覆盖率,你可以通过两种方式激活:

  1. 通过命令行传递标志“-coverage”
  2. 通过在 package.json 中配置 Jest

在使用 coverage 运行测试之前,请确保在 tests/filterByTerm.spec.js导入 filterByTerm

const filterByTerm = require("../src/filterByTerm");
// ...

保存文件并用 coverage 运行测试:

npm test -- --coverage

这是你得到的结果:

 PASS  __tests__/filterByTerm.spec.js
  Filter function
    ✓ it should filter by a search term (link) (3ms)
    ✓ it should filter by a search term (uRl) (1ms)
    ✓ it should throw when searchTerm is empty string (2ms)
-----------------|----------|----------|----------|----------|-------------------|
File             |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files        |     87.5 |       75 |      100 |      100 |                   |
 filterByTerm.js |     87.5 |       75 |      100 |      100 |                 3 |
-----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total

这是对我们的功能测试范围的一个很好的总结。如你所见第3行被uncovered。你可以尝试通过测试我添加的新语句来达到100%的代码覆盖率。

如果你想保持代码覆盖率始终处于活动状态,请在 package.json 中配置 Jest,如下所示:

  "scripts": {
    "test": "jest"
  },
  "jest": {
    "collectCoverage": true
  },

你还可以将标志传递给测试脚本:

  "scripts": {
    "test": "jest --coverage"
  },

还有一种可以获得代码覆盖率的HTML报告的方法,它就像配置Jest一样:

  "scripts": {
    "test": "jest"
  },
  "jest": {
    "collectCoverage": true,
    "coverageReporters": ["html"]
  },

现在,每次运行 npm test 时,你都可以在项目文件夹中访问名为 coverage 的新文件夹:getting-started-with-jest/coverage/。在该文件夹中,你将找到一堆文件,其中 /coverage/index.html 是代码覆盖范围的完整HTML摘要。

clipboard.png

如果单击函数名称,你还会看到确切的未经测试的代码行:

clipboard.png

很整洁不是吗?使用代码覆盖,你可以在有疑问时发现要测试的内容

如何测试 React?

React 是一个非常流行的 JavaScript 库,用于创建动态用户界面。 Jest 可以顺利地测试 React 应用(Jest 和 React 均来自 Facebook 的工程师)。 Jest 也是 Create React App 中的默认测试器。

如果你想学习如何测试React组件,请查看测试React组件:最明确的指南。该指南涵盖了单元测试组件、类组件、带hook的功能组件和新的 Act API。

结论(从这里开始)

测试是一个很大而且引人入胜的话题。有许多类型的测试和用于测试的库。在这个 Jest 教程中,你学习了如何为覆盖率报告配置 Jest,如何组织和编写简单的单元测试,以及如何测试 JavaScript 代码。

要了解有关 UI测试的更多信息,我强烈建议你查看用 Cypress 进行 JavaScript 端到端测试

即使它与 JavaScript 无关,我也建议阅读 Harry Percival 的使用 Python 进行测试驱动开发。它包含了所有测试内容的提示和技巧,并深入介绍了所有不同类型的测试。

如果你已准备好再迈出一步,要了解自动化测试和持续集成那么JavaScript中的自动化测试和持续集成是为你准备的。

你可以在Github上找到本教程的代码:getting-started-with-jest以及练习的解决方案。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎继续阅读本专栏其它高赞文章:


你可能感兴趣的

载入中...