开始使用 Jest 测试你的 JavaScript 代码

2025-05-25

开始使用 Jest 测试你的 JavaScript 代码

为什么我们要为我们的代码编写测试?

当多个开发人员同时对代码库进行修改时,问题和错误就容易出现。此外,排查谁提交了错误的代码,或者确切地找出错误的根源也很困难。因此,在将任何错误引入代码库之前,最好采取预防措施。这可以通过编写测试来实现,可以由各个开发人员在自己的机器上进行本地测试,或者也可以在 CI/CD 流水线中设置自动测试套件,当代码提交时触发。编写测试的另一个好处是,当我们开发应用程序的功能时,我们倾向于编写更好、更纯粹的函数,因为我们意识到最终必须为它们编写测试。

不同类型的测试

测试有多种类型,其中最常见的是:

单元测试
单元测试用于测试源代码的最小单元(例如函数或方法)。这是最容易实现的,也是所有测试类型中最常见的测试。

集成测试
:用于测试代码库中不同组件或单元之间的交叉通信,例如涉及应用程序架构不同部分的身份验证功能。集成测试是在各个单元测试完成的前提下进行的。

端到端测试
端到端测试,顾名思义,就是测试软件从头到尾的工作流程。当应用程序规模变大时,测试过程会变得非常复杂,因此许多公司仍然进行手动测试。这个过程可以从启动浏览器开始,在地址栏中输入 Web 应用程序的 URL……,这些操作都是由 UI 驱动的。不过,也有像 Selenium、Cypress 和 Protractor 这样的工具可以帮助自动化这些端到端测试,尽管设置起来可能需要相当长的时间。

市面上有很多测试库,它们服务于不同的目的,并适用于不同的编程语言。本文将重点介绍 JavaScript 代码的测试。更具体地说,Jest是本文的主角。

笑话:是什么以及为什么?

Jest 是一个流行的 JavaScript 测试库(尤其适用于 React 库)。它提供了丰富的方法和功能,涵盖了测试过程中的诸多方面,包括断言、模拟和间谍、代码覆盖率等等。当你使用 create-react-app 框架时,Jest 已经内置其中。在今天的文章中,我们将介绍如何在 JavaScript 代码中简单设置 Jest,以及如何在本地测试我们的应用功能。

快速设置

首先,我们用 npm 初始化工作目录。

npm init -y
Enter fullscreen mode Exit fullscreen mode

-y 标志基本上意味着自动接受来自 npm init 的提示(而不是在每个提示符后按 Enter 键)。

接下来,我们从 npm 安装 Jest。我们只需要将 Jest 安装为开发依赖项,因为它仅在开发阶段需要。

npm install jest --save-dev
Enter fullscreen mode Exit fullscreen mode

安装后,您应该会看到 Jest 包包含在 package.json 的 devDependencies 中。

{
  "name": "jest-testing",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^27.4.5"
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,让我们从第一个例子开始:

脚本1.js

const addNums = (a, b) => {
    return a + b;
};

module.exports = addNums;
Enter fullscreen mode Exit fullscreen mode

脚本 1 只是将两个数字相加并返回总和。

为了测试 script1.js,我们创建另一个名为“script1.test.js”的文件(最好遵循脚本测试文件的命名惯例)。在这个测试脚本中,我们可以添加以下 JavaScript 代码:

const addNums = require('./script1');

it('Function that adds two numbers and return sum', () => {
    expect(addNums(4, 5)).toBe(9);
    expect(addNums(4, 5)).not.toBe(10);
});
Enter fullscreen mode Exit fullscreen mode

这段代码的作用是,我们从 script1.js 导入 addNums 函数,并在该脚本中执行测试。你可以在 Jest 中编写“test”或其别名“it”(我们在脚本中使用的)来测试 addNums 函数。第一个参数是此特定测试的名称,第二个参数是要测试的期望值。该方法用简单的英语来说非常直观:期望函数将数字 4 和 5 相加,结果为 9。第二行测试是测试传入的 4 和 5 是否不应该产生 10 的结果。很简单。

为了运行这个测试,我们需要在 package.json 中配置“test”脚本来运行。您可以按如下方式配置:

"scripts": {
    "test": "jest ./*test.js"
  }
Enter fullscreen mode Exit fullscreen mode

这告诉 Node 运行测试,并捕获文件名的正则表达式。修改完成后,运行:

npm test
Enter fullscreen mode Exit fullscreen mode

您应该收到如下输出:

 PASS  ./script1.test.js

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.125 s
Ran all test suites matching /.\\*test.js/i.
Enter fullscreen mode Exit fullscreen mode

这意味着您现在有一个测试套件(script1.test.js)和一个测试(一个“它”就是一个测试)。

如果您不想每次都输入npm test来运行测试,您可以在 package.json 中配置测试脚本,如下所示:

"scripts": {
    "test": "jest --watch ./*test.js"
  }
Enter fullscreen mode Exit fullscreen mode

每次您在更改后保存文件时,npm test 都会监视并自动触发运行测试。

我们来看第二个例子:

script2.js

const findNames = (term, db) => {
    const matches = db.filter(names => {
        return names.includes(term);
    });
    // We only want the first three of search results.
    return matches.length > 3 ? matches.slice(0, 3) : matches;
}

const functionNotTested = (term) => {
    return `Hello ${term}!`;
};

module.exports = findNames;
Enter fullscreen mode Exit fullscreen mode

给定一个数据库(一个 JS 数组)和一个搜索词,返回与该词匹配的名称(仅前 3 个匹配)。我们将数据库作为依赖项注入到此函数中,是为了提高此函数的可复用性,并更方便使用模拟数据库进行测试。
函数“functionNotTested”没有任何用途,只是为了稍后向您展示测试覆盖率。我们不会为此函数编写测试。

这个函数似乎还有更多需要测试的地方。首先,我们可以测试函数是否使用提供的搜索词返回预期的搜索结果。其次,我们期望函数只返回搜索词的前 3 个匹配项。我们还可以检查函数中是否传入了 null 或 undefined 作为搜索词的参数,函数是否能够正确处理并返回空数组。最后,我们还可以确保此搜索函数区分大小写。由于这是一个单元测试,因此我们不需要执行真实的数据库连接。在测试与真实数据库的集成之前,我们应该确保此函数能够按预期处理注入的数据库数组和搜索词。因此,我们可以简单地创建一个模拟数据库数组,并将其传递给函数(这就是编写可重用代码的好处)。以下是我们可以构建的测试脚本:

const findNames = require('./script2');

const mockDB = [
    "Kamron Rhodes",
    "Angelina Frank",
    "Bailee Larsen",
    "Joel Merritt",
    "Mina Ho",
    "Lily Hodge",
    "Alisha Solomon",
    "Frank Ho",
    "Cassidy Holder",
    "Mina Norman",
    "Lily Blair",
    "Adalyn Strong",
    "Lily Norman",
    "Minari Hiroko",
    "John Li",
    "May Li"
]

describe("Function that finds the names which match the search term in database", () => {

    it("Expected search results", () => {
        // This should return empty array as "Dylan" does not exist in the mockDB
        expect(findNames("Dylan", mockDB)).toEqual([]);
        expect(findNames("Frank", mockDB)).toEqual(["Angelina Frank", "Frank Ho"]);
    });

    it("This should handle null or undefined as input", () => {
        expect(findNames(undefined, mockDB)).toEqual([]);
        expect(findNames(null, mockDB)).toEqual([]);
    });

    it("Should not return more than 3 matches", () => {
        expect(findNames('Li', mockDB).length).toEqual(3);
    })

    it("The search is case sensitive", () => {
        expect(findNames('li', mockDB)).toEqual(["Angelina Frank", "Alisha Solomon"])
    })
})
Enter fullscreen mode Exit fullscreen mode

这对你来说应该完全合理。如果函数遇到不存在的搜索词,或者收到 null 或 undefined 作为搜索词,函数应该返回空数组(JavaScript 的“filter”函数会处理这种情况)。在最后一个测试中,我们期望搜索函数区分大小写,因此诸如“Lily ...”和“... Li”之类的名称不应该出现在结果中。最后,“describe”函数用于将多个测试组合成一个整体。因此,当打印结果时,这些测试将具有一个名为“在数据库中查找与搜索词匹配的名称的函数”的组名。“toEqual”可用于测试 JavaScript 对象。

让我们来看最后一个例子:

script3.js

const fetch = require('isomorphic-fetch');

const fetchPokemon = async (pokemon, fetch) => {
    const apiUrl = `https://pokeapi.co/api/v2/pokemon/${pokemon}`;
    const results = await fetch(apiUrl);
    const data = await results.json();
    return {
        name: data.name,
        height: data.height,
        weight: data.weight
    };
};

module.exports = fetchPokemon;
Enter fullscreen mode Exit fullscreen mode

我们将需要在第三个脚本中调用 API,因为我们使用的是 Node.js(并且浏览器的 fetch API 不可用),您可以为 Node.js 安装 isomorphic-fetch:

npm install isomorphic-fetch
Enter fullscreen mode Exit fullscreen mode

本例中使用的 API 是 PokéAPI。通过将要查找的 Pokemon 传入 API 路径,可以方便地检索 Pokemon 信息。此函数返回找到的 Pokemon 的名称、体重和身高。

到目前为止,我想介绍 Jest 的另一个功能:为您的代码提供测试覆盖率的整体视图。

创建“script3.js”后,运行以下命令:

npm test -- --coverage
Enter fullscreen mode Exit fullscreen mode

你应该看到这个:

图片描述

这显示了每个 JavaScript 文件的测试覆盖率,以及哪些行未被覆盖。请记住,我们的 script2.js 中有一个函数我们没有为其编写任何测试,这就是为什么 script2.js 没有达到 100% 的原因。我们也没有为 script3.js 编写任何测试用例,因此它的测试覆盖率为 0%。

好了,我们可以开始为script3.js编写测试了,我们先用这个测试脚本试试:

const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');

it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
    fetchPokemon("bulbasaur", fetch).then(data => {
        expect(data.name).toBe("bulbasaur");
        expect(data.height).toBe(7);
        expect(data.weight).toBe(69);
    });
})
Enter fullscreen mode Exit fullscreen mode

所以,这个脚本试图调用 API,并检索要与预期值进行比较的数据。让我们尝试运行npm test

> jest-testing@1.0.0 test C:\Users\Dylan Oh\source\repos\jest-testing
> jest ./*test.js

 PASS  ./script2.test.js
 PASS  ./script3.test.js
 PASS  ./script1.test.js

Test Suites: 3 passed, 3 total                                                                                                                                                                                                   
Tests:       6 passed, 6 total
Snapshots:   0 total
Time:        0.801 s, estimated 1 s
Ran all test suites matching /.\\*test.js/i.
Enter fullscreen mode Exit fullscreen mode

耶!通过了!或者……真的吗?

嗯,有一种方法可以知道这一点。我们可以添加一个函数来检查测试中通过了多少个断言:

expect.assertions(numberOfAssertionsExpected);
Enter fullscreen mode Exit fullscreen mode

让我们将其添加到我们的 script3.test.js 中:

const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');

it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
    expect.assertions(3);
    fetchPokemon("bulbasaur", fetch).then(data => {
        expect(data.name).toBe("bulbasaur");
        expect(data.height).toBe(7);
        expect(data.weight).toBe(69);
    });
})
Enter fullscreen mode Exit fullscreen mode

我们期望这里完成 3 个断言,分别针对姓名、体重和身高。运行npm test

 FAIL  ./script3.test.js
  ● Find the Pokemon from PokeAPI and return its name, weight and height

    expect.assertions(3);

    Expected three assertions to be called but received zero assertion calls.

      3 |
      4 | it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
    > 5 |     expect.assertions(3);
        |            ^
      6 |     fetchPokemon("bulbasaur", fetch).then(data => {
      7 |         expect(data.name).toBe("bulbasaur");
      8 |         expect(data.height).toBe(7);

      at Object.<anonymous> (script3.test.js:5:12)

 PASS  ./script2.test.js
 PASS  ./script1.test.js

Test Suites: 1 failed, 2 passed, 3 total                                                                                                                                                                                         
Tests:       1 failed, 5 passed, 6 total
Snapshots:   0 total
Time:        0.842 s, estimated 1 s
Ran all test suites matching /.\\*test.js/i.
npm ERR! Test failed.  See above for more details.
Enter fullscreen mode Exit fullscreen mode

哎呀……零个断言调用。这是怎么回事?原因是,断言对异步调用一无所知,而且在数据返回之前,测试就已经通过了。因此,我们需要一种方法来告诉这些断言等待数据返回。

解决此问题的一种方法是将“done”函数传递给测试方法的回调函数,并将其放在断言之后。

const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');

it("Find the Pokemon from PokeAPI and return its name, weight and height", (done) => {
    expect.assertions(3);
    fetchPokemon("bulbasaur", fetch).then(data => {
        expect(data.name).toBe("bulbasaur");
        expect(data.height).toBe(7);
        expect(data.weight).toBe(69);
        done();
    });
})
Enter fullscreen mode Exit fullscreen mode

并且,它通过并确保进行了三次断言调用。

 PASS  ./script3.test.js
 PASS  ./script2.test.js
 PASS  ./script1.test.js

Test Suites: 3 passed, 3 total                                                                                                                                                                                                   
Tests:       6 passed, 6 total
Snapshots:   0 total
Time:        0.868 s, estimated 1 s
Ran all test suites matching /.\\*test.js/i.
Enter fullscreen mode Exit fullscreen mode

甚至更简单的方法是,我们可以只返回这个异步函数,而 Jest 足够智能,可以等到结果返回。

const fetch = require('isomorphic-fetch');
const fetchPokemon = require('./script3');

it("Find the Pokemon from PokeAPI and return its name, weight and height", () => {
    expect.assertions(3)
    return fetchPokemon("bulbasaur", fetch).then(data => {
        expect(data.name).toBe("bulbasaur");
        expect(data.height).toBe(7);
        expect(data.weight).toBe(69);
    });
})
Enter fullscreen mode Exit fullscreen mode

这也将符合断言测试的预期。我个人建议使用 return 语句返回 Promise,并且始终记住包含测试异步函数所需的断言调用次数,以确保断言确实被执行。

我们可以删除 script2.js 中不需要的函数,然后再次运行npm test -- --coverage :

图片描述

我们的测试覆盖率已达到 100%。

为代码编写测试始终是一个好习惯,无论是在本地测试还是在 CI/CD 流水线中测试。这将帮助我们更早地发现潜在的错误,并在某种程度上迫使我们编写更好的代码。

在我的下一篇文章中,我将介绍 React 组件的具体测试(例如快照测试)。谢谢。

请关注我,获取更多关于网页设计、编程和自我提升的文章😊

在 Medium 上关注我

文章来源:https://dev.to/ohdylan/start-testing-your-javascript-codes-with-jest-2gfm
PREV
7 个 CSS 技巧助你保持理智
NEXT
React 组件测试