使用 Jest 测试 JavaScript - 单元测试

2025-06-07

使用 Jest 测试 JavaScript - 单元测试

介绍

过去几周,我一直沉浸在测试我的 JavaScript 和 Python 项目的世界里,我的天哪!这太有趣了,我真不敢相信自己怎么没早点开始学。

我逐渐意识到,测试代码对于编写可维护、可复用且模块化的代码至关重要。它还能让任何贡献者、同事以及与我们合作的其他人几乎完全确信他们的新代码coolAndGreatFunction420()不会破坏我们的整个项目。

本文将介绍测试的工作原理、用途以及如何使用 jest 在我们的 Node.js 应用程序中实现它。


什么是测试?

测试代码是确保软件按照预期运行的过程。测试代码可以帮助我们对最终产品更加放心。

例如,如果我们有一个程序,其目的是将 2 + 2 相加并返回 4,我们希望确保它确实能做到这一点。我们不希望它返回 5、1 或“cuatro”,我们希望它返回 4。测试使我们能够确保该程序每次运行时都能按预期运行。

测试软件的形式和规模各不相同。例如,我们可以通过简单地像用户一样使用上述程序来测试它。我们可以启动终端、浏览器或任何类型的 GUI,并多次运行该程序,确保它始终返回预期值。然而,最有趣的测试是自动化测试

自动化测试就是用代码来测试代码。是不是很棒?这可以通过使用允许我们编写测试代码的框架来实现。

虽然本文主要关注自动化测试,但我认为手动测试程序仍然很重要。这样才能确保最终用户获得最佳的产品体验。

需要注意的是,无论测试多么深入或复杂,都无法确保代码无缺陷。然而,我坚信测试能够提升代码质量,最终打造出更优质的产品。


测试类型

在进入实际示例之前,我们应该了解常见的测试类型。这些并非唯一存在的类型,但却是 JavaScript 世界中最流行的类型。

单元测试

单元测试涵盖代码块,确保它们按照预期的方式运行。单元可以是一个函数、一个类或整个模块。我个人建议单元测试仅限于函数,因为我会尝试先测试代码中最小的部分,但这并没有真正的规则。我们可以有两种类型的单元:

  1. 孤立或单独的单元:没有其他依赖关系的单元,其行为和/或输出仅取决于其中包含的块。

  2. 社交单元:这些单元具有依赖关系。它们的执行和可选输出依赖于其他单元。在测试时,这意味着我们必须确保它们的依赖关系能够按预期工作,然后再进行测试。

// This is an isolated unit
function myNameIs(nameString) {
    return `Will the real ${nameString} please stand up`;
};

// This is a sociable unit, because it depends on other units
function pleaseStandUp() {
    return myNameIs("Slim Shady") + "please stand up, please stand up";
};
Enter fullscreen mode Exit fullscreen mode

集成测试

单元测试通过并不意味着我们的应用程序功能齐全、功能完备。一旦我们确定所有单元都经过了适当的测试,并且能够独立运行,我们就会按照它们在软件中的使用方式对它们进行集成测试。这就是集成测试。将这些单元组合在一起并进行测试,可以确保我们的函数、类和模块能够良好地协同工作。

端到端测试(E2E)

端到端测试(E2E)会对我们的应用程序进行从头到尾的全面测试。我的意思是,这种测试关注的是用户使用软件时的体验。

还记得我说过,即使我们已经设置了自动化测试,手动测试仍然很重要吗?嗯,端到端测试本质上就是自动化的手动测试(试着向非开发人员解释一下)。这些测试通常在无头浏览器中进行,尽管它们也可以在带有图形用户界面的浏览器中运行。通过我们的测试,我们尝试尽可能多地复制用户与我们网站的交互,并确保输出符合我们的预期。

除了复制用户在网站上的导航流程之外,我实际上还喜欢尝试在这些类型的测试中打破常规,就好像我是一个在网站上疯狂打字和点击的用户一样。


使用 Jest 进行单元测试

Jest是 Facebook 的一款开源产品,它使我们能够在几乎任何我们喜欢的 JavaScript 框架中编写和运行测试。

📖 注意

我知道,我不喜欢 Jest —— 以及 React —— 是 Facebook 的产品。然而,我必须承认它们是好产品,而 Facebook 的主题就另当别论了。

要在我们的项目中安装和使用 Jest,我们可以运行:

$ npm i -D jest
Enter fullscreen mode Exit fullscreen mode

然后我们可以添加一个测试脚本到我们的package.json

"scripts": {
    "test": "jest"
}
Enter fullscreen mode Exit fullscreen mode

每当运行 Jest 时,它都会自动查找并运行以 结尾的文件.test.js.spec.js或目录.js内的任何文件__tests__

现在,让我们开始编写要测试的单元。别担心,这些代码可能看起来很简单,但它们是我在实际项目中用到的实际函数。

// helpers.js
function isNumber(possibleNumber) {
    return typeof possibleNumber === "number";
};

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

好了,一个非常简单的函数,应该不难测试……对吧?让我们尝试编写第一个测试。在本例中,我们假设测试文件与 helpers.js 模块位于同一目录中。

// helpers.test.js
const isNumber = require("./helpers");

test("should return true if type of object is a number", () => {
    expect(isNumber(5)).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

这就是一个基本的 Jest 文件的样子。我们导入想要测试的模块/类/函数,指定一些我们期望的测试结果的描述,然后我们告诉 Jest 我们预期的函数结果是什么。让我们稍微分解一下。

  • test()是一个 Jest 函数,它定义要运行的单个测试。您可以test在一个文件中包含任意数量的语句。它需要两个必需参数和一个可选的第三个参数。第一个参数是测试名称。通常使用它来清楚地描述所要测试的内容。第二个参数是一个函数,其中包含我们的测试主体。我们在这里告诉 Jest 我们对测试的期望。在这种情况下,我们期望的返回值为第三个参数是一个可选值,以毫秒为isNumber(5)单位。由于测试通常非常快,我们预计任何单个测试都不会花费超过 5 秒的时间,这是默认值。truetimeouttimeout

  • expect()是我们用来实际测试期望值的函数。我们将其expect与“匹配器”函数一起使用,该函数会断言某个值的特定条件。在这个测试中,我们使用了toBe()匹配器,它将实际值与我们的期望值进行比较。匹配器有很多,我在这里只介绍其中几个,但您可以在其文档的 Jest 匹配器部分中阅读更多相关信息。

📖 注意

测试中可以包含expect()任意数量的语句,没有限制!建议您只使用合理数量的语句,因为您可以编写test()任意数量的语句,并且将语句expect()与其各自的测试用例组织在一起会更具可读性。

现在我们已经编写了第一个测试,我们可以运行npm run test并看到奇迹发生:

$ npm run test

> testing-javascript-with-jest@1.0.0 test
> jest

 PASS  ./helpers.test.js
  ✓ should return true if type of object is a number (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.279 s, estimated 1 s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

📖 注意

“测试套件”是指 jest 运行的文件数量,而“测试”是指test()在这些文件中找到的所有语句。

正如我之前所说,Jest 会自动查找并运行源代码中的所有测试文件,而且速度非常快。恭喜你编写了你的​​第一个单元测试!

让我们为此功能编写更多测试,以确保我们涵盖尽可能多的用例。

// helpers.test.js
const isNumber = require("./helpers");

test("should return true if type of object is a number", () => {
    expect(isNumber(0)).toBe(true);
    expect(isNumber(5)).toBe(true);
    expect(isNumber(+"5")).toBe(true);
});

test("should return false if type of object is not a number", () => {
    expect(isNumber(null)).toBe(false);
    expect(isNumber("number")).toBe(false);
    expect(isNumber(undefined)).toBe(false);
});
Enter fullscreen mode Exit fullscreen mode

我们npm run test再次奔跑并且……

$ npm run test
...

 PASS  ./helpers.test.js
  ✓ should return true if type of object is a number (2 ms)
  ✓ should return false if type of object is not a number

...
Enter fullscreen mode Exit fullscreen mode

太棒了!我们的函数似乎按预期运行了。


分组测试describe()

我们可以像刚才那样,只在顶层编写测试。然而,我们可以看到,尽管可以看到测试描述及其结果,但我们无法通过终端输出判断测试的是哪个单元。为了更好地说明这一点,我们再编写一个函数,helpers.js并将其相应的测试添加到 中helpers.test.js

// helpers.js
...

function isObject(possibleObject) {
    return typeof possibleObject === "object";
};

module.exports = { isNumber, isObject };
Enter fullscreen mode Exit fullscreen mode
// helpers.test.js
const { isNumber, isObject } = require("./helpers");
...

test('should return true if type of object is "object"', () => {
    expect(isObject({})).toBe(true);
    expect(isObject([])).toBe(true);
});

test('should return false if type of object is not "object"', () => {
    expect(isObject(5)).toBe(false);
    expect(isObject("object")).toBe(false);
});
Enter fullscreen mode Exit fullscreen mode

我们npm run test再次运行并获得了预期的(哈,明白了吗?)结果:

$ npm run test

> testing-javascript-with-jest@1.0.0 test
> jest

 PASS  ./helpers.test.js
  ✓ should return true if type of object is a number (1 ms)
  ✓ should return false if type of object is not a number (1 ms)
  ✓ should return true if type of object is "object" (1 ms)
  ✓ should return false if type of object is not "object" (1 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        0.204 s, estimated 1 s
Ran all test suites.
Enter fullscreen mode Exit fullscreen mode

正如我之前所说,虽然这些结果很棒,而且我们得到了所有绿色的勾选标记,但它们的可读性并不好,而且我们不知道哪个测试属于哪个单元。有一种更好的方法来组织我们的测试,以便终端的输出更清晰、更易读。

通过使用describe(),我们可以将测试分组到一个代码块下,从而将它们置于相同的作用域下——这在以后会很有用。要describe()在现有测试中实现该功能,我们只需用describe()语句包裹一组相关的test()语句即可。

// helpers.test.js
...

describe("isNumber", () => {
    test("should return true if type of object is a number", () => {
        expect(isNumber(0)).toBe(true);
        expect(isNumber(5)).toBe(true);
        expect(isNumber(+"5")).toBe(true);
    });

    test("should return false if type of object is not a number", () => {
        expect(isNumber(null)).toBe(false);
        expect(isNumber("number")).toBe(false);
        expect(isNumber(undefined)).toBe(false);
    });
});

describe("isObject", () => {
    test('should return true if type of object is "object"', () => {
        expect(isObject({})).toBe(true);
        expect(isObject([])).toBe(true);
    });

    test('should return false if type of object is not "object"', () => {
        expect(isObject(5)).toBe(false);
        expect(isObject("object")).toBe(false);
    });
});
Enter fullscreen mode Exit fullscreen mode

这一次,当我们运行时npm run test,我们将看到以相同名称组织的测试组。

$ npm run test
...

 PASS  ./helpers.test.js
  isNumber
    ✓ should return true if type of object is a number (2 ms)
    ✓ should return false if type of object is not a number (1 ms)
  isObject
    ✓ should return true if type of object is "object" (1 ms)
    ✓ should return false if type of object is not "object" (1 ms)
Enter fullscreen mode Exit fullscreen mode

当将测试分组在一起时,终端输出和编写的代码都会变得更具可读性,并且出于在未来的文章中会变得重要的原因,它还将相关测试分组在同一范围内。

使用 Jest Each 运行多个测试用例

从 Jest 23 版本开始,我们已经能够eachtestdescribe函数上使用 该方法。允许我们使用“表列”中定义的值多次运行相同的测试。使用Spock Data Tableseach时,表可以是数组类型,也可以是模板字面量

我们可以使用包含不同值的多个语句来简化测试,expect如下所示:

//helpers.test.js
...

describe("isNumber", () => {
    // Instead of this:
    // test("should return true if type of object is a number", () => {
    //     expect(isNumber(0)).toBe(true);
    //     expect(isNumber(5)).toBe(true);
    //     expect(isNumber(+"5")).toBe(true);
    // });


    // We use this:
    const numbers = [0, 5, +"5"];
    test.each(numbers)("should return true since type of %j is a number", 
        numberToTest => {
            expect(isNumber(numberToTest)).toBe(true);
        });
Enter fullscreen mode Exit fullscreen mode

我知道这语法有点奇怪,但它能让我们用更少的代码轻松测试大量测试。在这种情况下,我们可以继续向numbers数组中添加值,并检查它们是否都返回,true而无需添加额外的expect()语句。

📖 注意

%j 是一个printf formatting specifier。当我们运行 jest 时,每个测试都会有一个唯一的名称,其特定值将通过参数传递给它的字符串注入table

让我们对所有测试都这样做:

// helpers.test.js
...

describe("isNumber", () => {
    const numbers = [0, 5, +"5"];
    const notNumbers = [null, "number", undefined];

    test.each(numbers)('should return true since type of %j is "number"',
        possibleNumber => {
            expect(isNumber(possibleNumber)).toBe(true);
        });

    test.each(notNumbers)('should return false since type of %j is not "number"',
        possibleNumber => {
            expect(isNumber(possibleNumber)).toBe(false);
        });
});

describe("isObject", () => {
    const objects = [{}, []];
    const notObjects = [5, "object"];

    test.each(objects)('should return true since type of %j is "object"',
        possibleObject => {
            expect(isObject(possibleObject)).toBe(true);
            expect(isObject(possibleObject)).toBe(true);
        });

    test.each(notObjects)('should return false since type of %j is not "object"',
        possibleObject => {
            expect(isObject(possibleObject)).toBe(false);
            expect(isObject(possibleObject)).toBe(false);
        });
});
Enter fullscreen mode Exit fullscreen mode

现在,我们不仅节省了不必要的代码行,而且当打印到终端时,我们的测试都有唯一的名称:

$ npm run test
...

 PASS  ./helpers.test.js
  isNumber
    ✓ should return true since type of 0 is "number" (1 ms)
    ✓ should return true since type of 5 is "number"
    ✓ should return true since type of 5 is "number"
    ✓ should return false since type of null is not "number" (1 ms)
    ✓ should return false since type of "number" is not "number"
    ✓ should return false since type of undefined is not "number"
  isObject
    ✓ should return true since type of {} is "object"
    ✓ should return true since type of [] is "object"
    ✓ should return false since type of 5 is not "object"
    ✓ should return false since type of "object" is not "object"

...
Enter fullscreen mode Exit fullscreen mode

概括

这是一篇入门文章,我们学习了测试的基础知识、JavaScript 中最常见的测试类型,以及如何使用测试框架 Jest 测试我们的单元。现在我们知道,要测试代码,需要同时使用test()expect()函数。我们还知道,可以使用describe()函数将逻辑相似的测试归为同一范围,并使用 函数可以在不同的测试用例下重用相同的测试each

感谢您的阅读,下次再见!

📖 注意

这篇文章最初发表在我的网站上,如果您想阅读更多类似的内容,请去查看。

文章来源:https://dev.to/rafavls/testing-javascript-with-jest-unit-testing-jmb
PREV
创建 Node TypeScript 项目最简单的方法!
NEXT
是否可以验证 React 的 Context API 和 Hooks 是否有 Context API? Criando novo projeto Criando Contexto Extras