使用 Jest 在 Node.js 中进行单元测试和集成测试

使用 Jest 在 Node.js 中进行单元测试和集成测试

前言

大家好,欢迎来到今天的讲座!今天我们要聊一聊如何在 Node.js 项目中使用 Jest 进行单元测试和集成测试。如果你是第一次接触测试,或者对 Jest 还不太熟悉,别担心,我会尽量用轻松诙谐的语言,带你一步步了解 Jest 的强大功能,并且通过实际的代码示例,让你快速上手。

什么是测试?

在开始之前,我们先来简单了解一下什么是测试。测试是为了确保代码按预期工作的一种方法。想象一下,你写了一段代码,它应该能够计算两个数字的和。但是,你怎么知道这段代码真的能正确地计算呢?你可以手动输入一些数字,看看结果是否正确,但这显然不是一个高效的解决方案。更糟糕的是,如果你修改了代码,你怎么知道修改后的代码仍然能正常工作?

这就是测试的作用了!通过编写测试,我们可以自动验证代码的行为,确保它在各种情况下都能按预期工作。更重要的是,测试可以帮助我们在修改代码时保持信心,因为我们可以通过运行测试来确认没有引入新的错误。

单元测试 vs 集成测试

在测试的世界里,有两种常见的测试类型:单元测试集成测试

  • 单元测试:顾名思义,单元测试是对代码中的最小可测试单元(通常是函数或方法)进行测试。它的目标是确保每个单元都能独立工作,而不依赖于其他部分。单元测试通常非常快,因为它们只关注单个功能,不需要与外部系统(如数据库、API 等)交互。

  • 集成测试:集成测试则是为了验证多个模块或组件之间的协作是否正常。它会模拟真实的环境,测试不同部分之间的交互。集成测试通常比单元测试慢一些,因为它可能涉及到与外部系统的交互,但它能更好地反映应用程序在实际使用中的表现。

为什么要使用 Jest?

Jest 是一个由 Facebook 开发的 JavaScript 测试框架,专为现代 JavaScript 应用程序设计。它不仅支持单元测试和集成测试,还提供了许多强大的功能,比如:

  • 零配置:Jest 可以自动发现并运行测试文件,无需复杂的配置。
  • 快照测试:Jest 提供了快照测试功能,可以轻松捕获 UI 或数据结构的变化。
  • Mocking:Jest 内置了强大的 mocking 功能,可以轻松模拟外部依赖。
  • 并行执行:Jest 可以并行运行测试,大大提高了测试速度。
  • 丰富的断言库:Jest 提供了简洁易用的断言库,帮助你快速编写测试用例。

接下来,我们将通过几个实际的例子,详细介绍如何在 Node.js 项目中使用 Jest 进行单元测试和集成测试。


安装 Jest

首先,我们需要在项目中安装 Jest。假设你已经有一个 Node.js 项目,并且已经初始化了 package.json 文件。如果没有,可以使用以下命令创建一个新的 Node.js 项目:

npm init -y

接下来,安装 Jest:

npm install --save-dev jest

安装完成后,Jest 就已经准备好使用了!你可以通过以下命令运行 Jest:

npx jest

如果你想让 Jest 成为项目的默认测试命令,可以在 package.json 中添加一个 test 脚本:

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

现在,你只需要运行 npm test 就可以启动 Jest 了。


编写第一个单元测试

让我们从最简单的例子开始——编写一个加法函数,并为其编写单元测试。

创建加法函数

src 目录下创建一个名为 math.js 的文件,并编写一个简单的加法函数:

// src/math.js
function add(a, b) {
  return a + b;
}

module.exports = { add };

编写单元测试

接下来,我们在 __tests__ 目录下创建一个名为 math.test.js 的文件,并为 add 函数编写单元测试:

// __tests__/math.test.js
const { add } = require('../src/math');

test('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3);
});

test('adds -1 + 1 to equal 0', () => {
  expect(add(-1, 1)).toBe(0);
});

解释代码

  • test 是 Jest 提供的一个函数,用于定义测试用例。每个 test 函数接受两个参数:测试的描述字符串和一个包含测试逻辑的回调函数。
  • expect 是 Jest 的断言库,用于检查函数的返回值是否符合预期。toBe 是一个匹配器,用于比较两个值是否相等。

运行测试

保存文件后,运行以下命令来执行测试:

npm test

如果一切顺利,你应该会看到类似如下的输出:

PASS  __tests__/math.test.js
✓ adds 1 + 2 to equal 3 (5 ms)
✓ adds -1 + 1 to equal 0 (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.794 s
Ran all test suites.

恭喜你!你刚刚成功编写并运行了你的第一个 Jest 测试 😊


测试异步代码

在现实世界中,很多代码都是异步的,比如与数据库交互、调用 API 等。Jest 也支持测试异步代码,接下来我们来看看如何处理这种情况。

创建异步函数

假设我们有一个异步函数 fetchData,它会从某个 API 获取数据。为了简化示例,我们使用 setTimeout 模拟异步操作:

// src/api.js
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ data: 'Hello, World!' });
    }, 1000);
  });
}

module.exports = { fetchData };

编写异步测试

__tests__ 目录下创建一个名为 api.test.js 的文件,并为 fetchData 函数编写测试:

// __tests__/api.test.js
const { fetchData } = require('../src/api');

test('fetches data after 1 second', async () => {
  const result = await fetchData();
  expect(result).toEqual({ data: 'Hello, World!' });
});

解释代码

  • async/await 是 JavaScript 中处理异步代码的语法糖。async 关键字将函数标记为异步函数,而 await 关键字用于等待 Promise 的完成。
  • toEqual 是另一个匹配器,用于比较两个对象是否相等。

运行测试

保存文件后,再次运行 npm test。由于 fetchData 是异步的,Jest 会等待 Promise 完成后再进行断言。


Mocking 外部依赖

在编写单元测试时,我们通常不希望依赖外部系统(如数据库、API 等),因为这会使得测试变得复杂且不可控。Jest 提供了强大的 mocking 功能,可以帮助我们模拟这些外部依赖。

模拟 HTTP 请求

假设我们有一个函数 getUser,它会从某个 API 获取用户信息。我们不想在测试中真正调用这个 API,而是想模拟它的行为。

// src/user.js
const axios = require('axios');

async function getUser(id) {
  const response = await axios.get(`https://api.example.com/users/${id}`);
  return response.data;
}

module.exports = { getUser };

编写带有 Mock 的测试

__tests__ 目录下创建一个名为 user.test.js 的文件,并为 getUser 函数编写测试:

// __tests__/user.test.js
const { getUser } = require('../src/user');
const axios = require('axios');

jest.mock('axios'); // 模拟 axios

test('gets user with ID 1', async () => {
  // 定义 mock 返回值
  axios.get.mockResolvedValue({
    data: { id: 1, name: 'Alice' },
  });

  const user = await getUser(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
  expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
});

解释代码

  • jest.mock('axios') 用于模拟 axios 模块。这意味着在测试中,axios.get 不会真正发起 HTTP 请求,而是返回我们定义的 mock 数据。
  • mockResolvedValue 是 Jest 提供的一个方法,用于定义 Promise 的返回值。
  • toHaveBeenCalledWith 是一个匹配器,用于检查 axios.get 是否被调用了正确的 URL。

运行测试

保存文件后,再次运行 npm test。这次,getUser 函数不会真正调用 API,而是使用我们定义的 mock 数据。


快照测试

有时候,我们想要测试一个复杂的对象或 UI 组件,手动编写断言可能会非常繁琐。Jest 提供了快照测试功能,可以轻松捕获并验证复杂的输出。

创建一个复杂的对象

假设我们有一个函数 createUserProfile,它会根据用户信息生成一个复杂的对象:

// src/profile.js
function createUserProfile(user) {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    createdAt: new Date().toISOString(),
  };
}

module.exports = { createUserProfile };

编写快照测试

__tests__ 目录下创建一个名为 profile.test.js 的文件,并为 createUserProfile 函数编写快照测试:

// __tests__/profile.test.js
const { createUserProfile } = require('../src/profile');

test('creates user profile', () => {
  const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
  const profile = createUserProfile(user);

  // 生成快照
  expect(profile).toMatchSnapshot();
});

解释代码

  • toMatchSnapshot 是一个特殊的匹配器,用于生成并保存当前对象的快照。如果后续的测试中对象发生了变化,Jest 会自动检测到差异并提示你更新快照。
  • 第一次运行测试时,Jest 会自动生成一个快照文件,保存在 __snapshots__ 目录下。

运行测试

保存文件后,运行 npm test。Jest 会生成一个快照文件,并将其保存在 __snapshots__ 目录下。下次运行测试时,Jest 会将当前的输出与快照进行比较,确保它们一致。


集成测试

接下来,我们来看看如何编写集成测试。集成测试的目标是验证多个模块或组件之间的协作是否正常。为了演示这一点,我们将创建一个简单的 Express 应用程序,并编写集成测试来验证其路由是否按预期工作。

创建 Express 应用

首先,在 src 目录下创建一个名为 app.js 的文件,并编写一个简单的 Express 应用:

// src/app.js
const express = require('express');
const app = express();

app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  const user = { id: userId, name: 'Alice' };

  res.json(user);
});

module.exports = app;

编写集成测试

__tests__ 目录下创建一个名为 app.test.js 的文件,并为 app 编写集成测试:

// __tests__/app.test.js
const request = require('supertest');
const app = require('../src/app');

describe('GET /users/:id', () => {
  test('returns user with ID 1', async () => {
    const response = await request(app).get('/users/1');

    expect(response.statusCode).toBe(200);
    expect(response.body).toEqual({ id: '1', name: 'Alice' });
  });
});

解释代码

  • supertest 是一个流行的 HTTP 断言库,可以帮助我们轻松测试 Express 应用程序。我们使用它来发送 HTTP 请求并验证响应。
  • describe 是 Jest 提供的一个函数,用于将多个相关测试组织在一起。每个 describe 块可以包含多个 test 用例。
  • request(app).get('/users/1') 发送一个 GET 请求到 /users/1 路由,并返回一个包含响应的 response 对象。
  • expect(response.statusCode).toBe(200) 检查响应的状态码是否为 200(表示请求成功)。
  • expect(response.body).toEqual({ id: '1', name: 'Alice' }) 检查响应的 JSON 数据是否符合预期。

运行测试

保存文件后,再次运行 npm test。Jest 会启动 Express 应用,并验证 /users/1 路由是否按预期工作。


测试覆盖率

编写测试固然重要,但我们也想知道哪些代码已经被测试覆盖,哪些代码还没有被测试到。Jest 提供了内置的代码覆盖率报告功能,可以帮助我们了解测试的覆盖率。

生成覆盖率报告

要生成覆盖率报告,只需在 package.json 中添加一个 coverage 脚本:

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

然后运行以下命令:

npm run coverage

Jest 会生成一个详细的覆盖率报告,并将其保存在 coverage 目录下。你可以打开 coverage/lcov-report/index.html 文件,查看每个文件的覆盖率情况。

解释覆盖率报告

  • Statements:语句覆盖率,表示有多少行代码被执行过。
  • Branches:分支覆盖率,表示有多少条件分支被执行过。
  • Functions:函数覆盖率,表示有多少函数被执行过。
  • Lines:行覆盖率,表示有多少行代码被执行过。

通过查看覆盖率报告,你可以发现哪些代码没有被测试到,并有针对性地编写更多测试用例。


总结

恭喜你完成了今天的讲座!通过这次学习,你已经掌握了如何在 Node.js 项目中使用 Jest 进行单元测试和集成测试。我们从最简单的加法函数开始,逐步深入到异步代码、外部依赖的模拟、快照测试,最后还编写了集成测试并生成了覆盖率报告。

Jest 是一个非常强大的测试框架,它不仅简单易用,还提供了丰富的功能,帮助你编写高质量的测试代码。希望今天的讲座对你有所帮助,如果你有任何问题或建议,欢迎随时提问!

最后,记得测试不仅仅是编写代码的一部分,它更是确保代码质量的重要手段。写好测试,让你的代码更加可靠,也让开发过程更加愉快 😊

祝你编程愉快!🎉

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注