【译】Jest初学者教程:JavaScript测试入门

1,428 阅读5分钟

测试的意义是什么?

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

测试类型很多,但简单来说测试分为三大类:

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

在本Jest教程中,我们将仅介绍单元测试,但是在本文结尾,您将找到其他类型的测试的资源。

Jest教程:什么是Jest

Jest是JavaScript测试运行程序,即用于创建,运行和构建测试的JavaScript库。Jest是作为NPM软件包分发的,您可以将其安装在任何JavaScript项目中。 Jest是目前最受欢迎的测试运行程序之一(我觉得没有之一),也是Create React App的默认选择。

首先,我怎么知道要测试什么?

在测试方面,即使是简单的代码块也可能使新手懵逼。最常见的问题是“我怎么知道要测试什么?”。如果你正在编写Web应用程序,那么一个好的切入点就是测试应用程序的每个页面以及每个用户的交互。但是,Web应用程序也由功能和模块之类的代码单元组成,也需要进行测试。大多数情况下有两种情况:

  • 你继承了未经测试的旧代码
  • 你从0开始新实现的功能

该怎么做呢?对于这两种情况,你都可以通过将测试视为代码的一部分来进行检查,这些代码可以检查给定的函数是否产生预期的结果。典型的测试流程如下所示:

  • 导入到测试的功能
  • 给定一个输入
  • 定义期望的输出
  • 检查函数是否产生预期的输出

真的,就是这样。如果你从以下角度考虑,测试将不再可怕:输入-预期输出-声明结果。稍后,我们还将看到一个方便的工具,用于几乎准确地检查要测试的内容。现在先用Jest手动测试!

Jest教程: 初始化项目

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

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

下一步安装Jest:

npm i jest --save-dev

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

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

现在,你可以嗨皮的开始(入坑)了。

Jest教程:规范和测试驱动的开发

作为开发人员,我们都喜欢创造自由。但是,当涉及到严重的问题时,大多数时候没有那么多特权。通常,我们必须遵循规范,即对构建内容的书面或口头描述。

在本教程中,我们的项目经理提供了一个相当简单的规范。非常重要的一点是,业务方需要一个JavaScript函数,该函数用来过滤一个对象数组。

对于每个对象,我们必须检查一个名为“ url”的属性,如果该属性的值与给定的关键字匹配,则应在结果数组中包括匹配的对象。作为一个精通测试的JavaScript开发人员,你希望遵循TDD(测试驱动开发),这是一种在开始编写代码之前必须编写失败测试的准则。

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

cd getting-started-with-jest
mkdir __tests__

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

现在,让我们进行测试!

Jest教程:测试结构和第一个失败的测试

是时候创建你的第一个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
  });
});

至此,我们准备编写测试了。请记住,测试是输入,功能和预期输出的问题。首先让我们定义一个简单的输入,即对象数组:

  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 匹配器来检查虚拟函数(目前)在调用时是否返回了预期结果。这是测试:

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匹配器的更多信息,请查阅文档)。

到这里,你可以尝试:

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". 实际上,这是好事,让我们在下一节中修复它。

Jest教程: 修复测试(并再次让它失败)

真正缺少的是 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教程: 修复大小写问题

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

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

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

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

为了使其通过,我们可以调整提供的正则表达式以匹配:

//
    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);
  });
}

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

  • 测试搜索字词“ uRl”
  • 测试一个空的搜索词。函数应如何处理?

你将如何组织这些新测试?

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

Jest教程: 代码覆盖率

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

mkdir src && cd _$
touch filterByTerm.js

filterByTerm.js内容:

  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具有内置的代码覆盖范围,你可以通过两种方式激活它:

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

在进行覆盖测试之前,请确保将filterByTerm导入tests / filterByTerm.spec.js:

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

保存文件并进行覆盖测试:

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

对于我们的函数,一个非常好的总结。可以看到,第三行没有被测试覆盖到。下面来通过添加新的测试代码,让覆盖率达到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的新文件夹:jest / jest / coverage /。在该文件夹中,您会发现一堆文件,其中/coverage/index.html是代码覆盖率的完整HTML摘要:

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

非常整洁不是吗?通过代码覆盖,你可以发现有疑问时要测试的内容。

Jest教程:如何测试React?

React是一个超级流行的JavaScript库,用于创建动态用户界面。 Jest可以顺利地测试React应用(Jest和React都来自Facebook的工程师)。 Jest还是Create React App中的默认测试运行程序。

如果您想学习如何测试React组件,请查看《测试React组件:最权威指南》。该指南涵盖了单元测试组件,类组件,带有钩子的功能组件以及新的Act API。

结论(从这里开始)

测试是一个巨大而有趣的话题。有许多类型的测试和许多测试库。在本 Jest 教程中,你学习了如何配置 Jest 进行覆盖率报告,如何组织和编写简单的单元测试以及如何测试 JavaScript 代码。

要了解有关UI测试的更多信息,我强烈建议您看一下Cypress的JavaScript端到端测试

如果你准备好想入坑自动化测试和集成测试,那么在js中进行自动化测试和集成测试会非常适合你。

您可以在Github上找到本教程的代码:getting-started-with-jest

感谢您的阅读和关注!