← 返回蜂巢洞察

如何使用 Vitest 在 Express 中实现测试自动化

在不断切换标签页来测试应用程序的集成功能时,思考API的逻辑会让人感到不堪重负,而且非常耗时。 不过,你可以通过为应用程序编写测试用例来节省时间和精力——每当添加新功能时,都可以运行这些测试,而且在整个开发过程中都不需要离开集成开发环境。这样就能确保每个新功能都能按预期正常工作。 在本指南中,我将通过代码帮助你建立信心:首先你会学习如何使用测试用例来验证API的功能,然后会在Postman或其他API测试工具中确认测试结果。 先决条件 Node.js与Express的基础知识: 你应该具备Node.js和Express(或类似库如 fastify )的基本使用能力,包括如何构建和运行简单的AP

在不断切换标签页来测试应用程序的集成功能时,思考API的逻辑会让人感到不堪重负,而且非常耗时。

不过,你可以通过为应用程序编写测试用例来节省时间和精力——每当添加新功能时,都可以运行这些测试,而且在整个开发过程中都不需要离开集成开发环境。这样就能确保每个新功能都能按预期正常工作。

在本指南中,我将通过代码帮助你建立信心:首先你会学习如何使用测试用例来验证API的功能,然后会在Postman或其他API测试工具中确认测试结果。

先决条件

  • Node.js与Express的基础知识:你应该具备Node.js和Express(或类似库如fastify)的基本使用能力,包括如何构建和运行简单的API。

  • TypeScript的基础知识:本指南中的代码示例都是用TypeScript编写的,因此你需要掌握TypeScript的基本概念。

  • 对MongoDB的初步了解:虽然有帮助但并非必需。本指南中的示例使用MongoDB进行演示,但实际上这些逻辑适用于任何数据库或数据存储方案,只是使用的工具不同而已。

  • 实际的API开发经验:如果有使用Express编写后端API的经验,会帮助你更顺利地学习本指南的内容。

  • 求知欲与积极性:有意愿通过学习编写API测试用例来提升自己的后端开发技能。

  • 环境准备:确保你的计算机上安装了Node.js 20版本或更高版本

目录

本指南中使用的所有代码示例都可以在这个Git仓库中找到。每个示例都有独立的分支,如果你需要完整的示例代码,这些分支会被合并到主分支中。如果觉得这个仓库有帮助,请考虑给它添加星标。

关键测试概念

测试能帮助你对自己的代码更有信心。你会编写一些用于检测应用程序中其他代码行为的测试用例,或者用来验证某个功能是否按预期运行。

在本指南中,我们将介绍两种类型的测试:

  • 单元测试:这是最基础的测试类型,也是我认为最容易掌握的。你单独测试某一段代码,以确认它的行为是否符合预期。

  • 集成测试:这种测试方法用于验证应用程序的不同组件是如何协同工作的,以确保它们能按设计要求正常运行。

关键测试术语

在本指南中,我会使用一些与测试相关的技术术语,在开始讲解之前,我想先对这些术语进行说明:

  • 模拟:模拟是一种用于创建真实函数调用的假实现的技术。

  • 监视:监视是一种用来检查函数运行情况的方法,例如,可以查看该函数是否使用了某种类型的参数,或者它被调用了多少次。

  • 断言:断言用于验证实际得到的结果是否与预期结果一致。

好了,现在这些基本概念已经讲清楚了,接下来让我们来了解一下测试的一些基础知识,这样在开始编写测试代码之前,你们就能掌握相关的最佳实践。

如何命名测试文件

请确保你的测试文件遵循以下命名规则之一:

  • filename.test.ts

  • filename.spec.ts

  • filename.test.js

  • filename/spec.js

如果文件名中同时包含spectest这两个关键字,那么该文件就可以被执行。这些关键字也有助于测试框架正确地运行相应的测试文件。

你可以根据自己使用的语言来选择文件扩展名——是.ts还是.js。在本指南中,我使用的是TypeScript,因此测试文件的扩展名为.ts

测试文件的组成部分

通常来说,一个测试文件会包含四个主要的组成部分,你需要熟悉这些部分:

  • describe:用于描述你要编写的是哪一项测试。

  • it:用于指定被测试函数在测试时必须满足的条件。

  • expect:用于确定当你调用被测试的函数时,期望得到什么样的结果。

  • 匹配器:这些是expect关键字所提供的不同方法,我们使用它们来判断被测试函数返回的值是否符合预期的要求。

下面是一个测试文件的示例:

import { describe, it, expect } from "vitest";
import validateEmail from "../utils/email-validation";

describe("email validation test suites", () => {
  it("must define email validation function", () => {
    expect(validateEmail).toBeDefined();
  });
});

上述代码片段用于检测validateEmail函数是否已经被定义。

在这个测试文件中:

  • 我们使用describe来指定这项测试的描述内容。describe接收一个描述测试内容的字符串,以及一个用于处理不同测试用例的函数。

  • it关键字用于定义你要测试的具体功能。它接收一个描述具体测试内容的字符串,以及一个用于执行测试并处理断言结果的回调函数。

  • 然后expect会利用函数的返回类型,通过toBeDefined匹配器来检查该结果是否符合预期条件。

匹配器列表:

有许多匹配器可供您使用。以下是其中的一些:

  • toBe:比较传入的值与函数返回的值是否相同。它适用于字符串、数字等原始数据类型。

  • toEqual:比较传入的值与函数返回的值是否相同。它适用于对象、数组等非原始数据类型。

  • toThrow:用于检测某个是否抛出了预期的错误对象或实例。

  • toBeCalledWith:用于检查一个函数是否使用了特定的参数被调用。

  • toBecalledOnce:用于检查一个函数是否只被调用了一次。

  • toBeDefined:用于检查一个函数是否已经被定义。

  • toBeUndefined:用于检查一个函数是否返回了未定义的值。

  • toBeTruthy:用于检查一个函数是否返回了布尔值“true”。

  • toBeFalsy:用于检查一个函数是否返回了布尔值“false”。

以上只是众多匹配器中的一部分而已。

Vitest安装

现在您已经了解了一些测试基础知识,我们可以开始进行实际的测试了。首先,我们需要安装vitest——这个我们将用来运行和编写应用程序测试的框架。

您可以根据以下列表选择自己喜欢的包管理器来安装vitest:

pnpm add -D vitest # 适用于pnpm包管理器
npm install -D vitest # 适用于npm包管理器
yarn add -D vitest # 适用于yarn包管理器
bun add -D vitest # 适用于bun包管理器

单元测试

单元测试的重点是独立地测试应用程序中的某一小段代码。举个简单的例子:如果您有一个用于将联系信息添加到数据库中的函数,那么在测试时,您可以检查在将联系信息保存到数据库之前,该电子邮件地址是否有效。

让我们先来测试一个简单的电子邮件验证函数,这样您就能了解单元测试的工作原理以及如何编写这样的测试代码:

export default function validateEmail(email: string) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  if (regex.test(email)) {
    return true;
  } else {
    throw new Error("电子邮件格式无效");
  }
}

上述代码定义了一个函数,该函数接收一个电子邮件地址,然后使用正则表达式来验证这个地址是否有效。如果地址无效,就会抛出错误。

validateEmail函数的测试

首先,让我们来看看validateEmail函数在遇到有效的电子邮件地址时是否会返回“true”:

import { describe, it, expect } from "vitest";
import validateEmail from "../utils/email-validation";

describe("邮件验证测试套件", () => {

  it("对于有效的电子邮件,该函数会返回true", () => {
    const sampleEmail = "arnoldjabo@gmail.com";
    expect(validateEmail(sampleEmail)).toBe(true);
  });

});

在上面的测试中,我们创建了一个变量sampleEmail,用于在validateEmail函数中作为示例电子邮件使用。保存这些代码后,在终端中运行npx vitest,你应该会看到显示测试结果的界面,其效果如下图所示:

vitest测试终端中显示的通过测试的结果

当你运行测试时,Vitest会显示你正在测试的测试文件列表、执行了多少测试以及有多少测试通过了、又有多少测试失败了。

让我们再创建一个用于检测无效电子邮件的测试:

import { describe, it, expect } from "vitest";
import validateEmail from "../utils/email-validation";

describe("邮件验证测试套件", () => {

    it("对于无效的电子邮件,该函数会抛出错误", () => {
    const sampleEmail = "verymasd.com";
    const invalidEmailResults = () => validateEmail(sampleEmail);
    expect(invalidEmailResults).toThrow("无效的电子邮件格式");
  });

});

对于那些会抛出错误的函数,你需要将它们包裹在另一个函数内部,这样就可以防止这些错误在测试达到断言或expect部分之前就导致整个测试流程中断。

在上面的例子中,validateEmail函数被包裹在了另一个函数内部,这个外部函数会接收validateEmail函数抛出的任何错误,并将其存储在invalidEmailResults变量中。接下来,我们在expect断言中使用了toThrow匹配器,来检查validateEmail函数在遇到无效电子邮件时是否确实会抛出这种类型的错误。

如果你运行这个测试,现在应该会有两个测试通过:

当输入无效电子邮件时,该测试会抛出错误,但最终有两个测试通过了

如果validateEmail函数在抛出错误时没有被包裹在另一个函数内部,那么当你运行测试时,你会看到如下这样的结果:

如果没有将会抛出错误的函数包裹在另一个函数内部,你在测试终端中会看到的错误信息

如您所见,错误会在函数执行到最终验证步骤之前就被触发。因此,如果您的函数会抛出错误,请务必将其封装在另一个函数内部,以避免在测试过程中出现错误。

提示:在编写单元测试时,应重点关注函数的预期输入和输出结果。您无需关心函数的实现细节,因为输入和输出才是最重要的要素。

测试API调用

现在您应该已经了解了单元测试的工作原理。接下来,让我们看看如何对那些需要与数据库进行交互的API进行单元测试。

import Contacts from "..schema/contactList";
import { Request, Response } from "express";
import validateEmail from"..utils/email-validation";

async function addContacts(req: Request, res: Response) {
  const { contactName, phoneNumber, email } = req.body;
  validateEmail(email);

  const contact = await Contacts.create({ contactName, phoneNumber, email });

  return res.status(201).json({ message: `成功创建了${contact.contactName}` });
}

export default addContacts;

在上面的代码示例中,我们定义了一个用于向数据库添加联系人的函数,并且还会验证传入的电子邮件地址是否有效。

在这个例子中,我们使用mongoDB作为数据库,而Mongoose则用于将代码与mongoDB实例连接起来。

对于那些依赖于外部服务的函数(例如需要调用API或其他函数的函数),它们的单元测试方式会有所不同。

在这种情况下,我们需要为这些函数创建模拟版本,让它们在测试时能够返回预期的结果,从而帮助我们验证其功能是否正常。

我们将从模拟`validateEmail`函数的实现开始。这样做有助于您更深入地理解单元测试以及测试机制的一般原理。

注意:虽然模拟`validateEmail`函数并不是特别重要,因为直接使用原函数与模拟版本在结果上并没有太大差别,但为了帮助您学习,我们还是会对其进行模拟。

vi.mock这个工具可以帮助我们将某个模块的导入语句转换成模拟版本。使用时,我们需要提供该模块的路径以及一个用于生成模拟版本的回调函数。

vimock(filepath, callback);
vi Mock接受两个参数:要被模拟的模块的文件路径,以及一个用于生成模拟版本的回调函数。

让我们先来模拟`validateEmail`模块,并创建一个回调函数来将其导入语句转换为模拟版本:
vimock../utils/email-validation, () => {
  return { default: vi.fn>();
});
import validateEmail from "../utils/email-validation";

在回调函数(工厂函数)中,我们会返回一个导出模块的对象,该对象的键为default,值为vi.fn。在进行函数模拟时,我们使用vi.fn(),它会自动将函数的返回值替换为undefined

我们选择使用“default”作为键,是因为validateEmail函数是作为默认导出项被提供的。如果它是通过命名导出的方式提供的,那么在返回对象中我们应该使用实际的导出名称,而不是“default”。

vi.mock("../utils/email-validation", () => {
  return { validateEmail: vi.fn() };
});
import { validateEmail } from "../utils/email-validation";

在进行模块模拟操作之后,务必再导入被模拟的模块,这样才能避免使用到真实的模块内容。

vi.fn()提供了一些方法,这些方法可以帮助我们定义被模拟函数的实现逻辑及行为。其中一些方法包括:

  • mockReturnValue:用于为被模拟函数指定返回值

  • mockRejectsValue:对于基于Promise的函数,该方法用于指定函数应返回的错误信息。

  • mockResolveValue:对于基于Promise的函数,该方法用于指定函数应返回的数据。

  • mockImplementation:用于为被模拟函数定义新的实现逻辑。

  • mockReturnThis:用于返回被模拟函数的实际实例。

在定义模拟实现或设置模拟返回值时,这些方法通常是你会最常使用的。不过需要记住,实际上还有许多其他可用的方法。

对于emailValidate函数来说,我们将使用mockReturnValuemockImplementation这两种方法。

让我们使用mockReturnValue来让validateEmail函数在默认情况下返回true,前提是输入的电子邮件格式是正确的:

vi.mock "../utils/email-validation", () => {
  return { default: vi.fn().mockReturnValue(true) };
});
import validateEmail from "../utils/email-validation";

我们首先使用vi(fn创建了一个被模拟的函数,然后通过调用mockReturnValue(true)来修改这个被模拟函数的默认返回值,将其从undefined改为true

定义模拟实现逻辑

对于被模拟的函数,我们可以进行很多操作,比如为其定义新的实现逻辑,从而替换原函数中现有的处理逻辑。

让我们创建一个测试用例,当在创建联系人时提供错误的电子邮件地址时,该测试用例会抛出错误。

import { describe, expect, it, vi } from "vitest";
import mockinggoose from "mockingoose";
import Contacts from "../schema/contactList";
import addContacts from "../src/controllers/add-contacts.controller";

import { type Response, type Request } from "express";

vi.mock("../utils/email-validation", () => {
  return { default: vi.fn().mockReturnValue(true) };
});

import validateEmail from "../utils/email-validation";

const fakeContact = {
  contactName: "arnold",
  phoneNumber: 798600102,
  email: "arnoldjabo@gmail.com",
};

describe("将联系人添加到数据库中", async () => {
  it("当提供错误的电子邮件地址时,会抛出错误", async () => {
    const req = {
      body: { ...fakeContact, email: "fakemail" },
    } as Request;  

    const res = {
      status: vi.fn().mockReturnThis(),
      json: vi(fn),
    } as any as Response;

    (validateEmail as ReturnType<typeof vi.fn>>).mockImplementation(() => {
      throw new Error("无效的电子邮件地址!");
    });
 
    await expect(addContacts(req, res)).rejects.toThrow();
  });
});
在上面的代码片段中,我们创建了一个伪造的请求对象,并将其强制转换为`express`框架所认可的请求类型。对于响应对象,我们也采用了同样的方法进行模拟——但不同的是,在处理响应对象时,我们还模拟了在Express响应中常用的那些方法,比如状态码和`json`格式的数据。 接下来,我们将`validateEmail`函数的返回类型改为了vitest框架支持的模拟函数类型,这样就可以避免TypeScript抛出警告。然后我们使用`mockImplementation`方法,在`validateEmail`函数内部故意抛出一个新的错误。 在进行断言时,由于我们现在抛出的是一个基于Promise的错误,因此我们需要使用`rejects`方法,并接着连接另一个匹配器,来确定该函数最终会抛出哪种类型的错误。 **提示:** 在使用TypeScript进行开发时,响应对象不能像请求对象那样被随意转换类型。因为响应对象的类型规范要比请求对象严格得多。所以你首先需要将其转换为`any`类型,然后再转换回响应对象类型。这样既能避免TypeScript的警告,又能确保测试代码中的类型判断依然有效。 ## 模拟Mongoose模型 在进行单元测试时,我们并不希望将测试数据保存到真实的数据库中。相反,我们可以伪造那些调用数据库服务的函数的实现方式——在我们的例子中,我们可以使用Mongoose框架中的`create`方法,该方法会将记录保存到Mongo数据库中。然后我们可以指定这个方法在成功执行时应返回什么内容(这个返回结果应该与使用真实数据库时得到的结果完全一致)。 首先,我们需要安装一个用于模拟Mongoose模型的库,它的名称是`mockingoose`: ```bash pnpm add -D mockingoose # 对于pnpm包管理器 npm install -D mockingoose # 对于npm包管理器 yarn add -D mockingoose # 对于yarn包管理器 bun add -D mockingoose # 对于bun包管理器 ``` 安装完成后,我们可以为`Contacts`模型创建一个模拟版本: ```typescript import { describe, expect, it, vi } from "vitest"; import mockinggoose from "mockingoose"; import Contacts from "../schema/contactList"; const fakeContact = { contactName: "arnold", phoneNumber: 798600102, email: "arnoldjabo@gmail.com", }; describe("将联系人信息添加到数据库中", async () => { it("能够成功地将新的联系人信息添加到数据库中", async () => { mockinggoose(Contacts).toReturn(fakeContact, "save"); }); }); ``` 要模拟一个Mongoose模型,我们需要调用`mockinggoose()`函数,并将需要被模拟的模型作为参数传入。然后我们使用`toReturn`匹配器来指定该模拟方法应该返回什么结果。`toReturn`匹配器需要接收两个参数:一个是用于模拟的数据集,另一个是用于处理这些数据的Mongo数据库方法。在我们的例子中,我们使用了`save`方法,因为我们需要将新创建的记录保存到数据库中。

API单元测试

我们可以先编写针对“添加联系人”API调用的第一个测试用例,具体代码如下:

import { describe, expect, it, vi } from "vitest";
import mockinggoose from "mockingoose";
import Contacts from "../schema/contactList";
import addContacts from "../src/controllers/add-contacts.controller";
import { type Response, type Request } from "express";

vi.mock("../utils/email-validation", () => {
  return { default: vi.fn().mockReturnValue(true) };
});

import validateEmail from "../utils/email-validation";

const fakeContact = {
  contactName: "arnold",
  phoneNumber: 798600102,
  email: "arnoldjabo@gmail.com",
};

describe("向数据库中添加联系人", async () => {
  it("成功将新联系人添加到数据库中", async () => {
    mockinggoose(Contacts).toReturn(fakeContact, "save");

    const req = {
      body: fakeContact,
    } as Request;

    const res = {
      status: vi.fn().mockReturnThis(),
      json: vi(fn),
    } as any as Response;

    await addContacts(req, res);
    expect(res.status).toEqual(201);
    expect(res.json).toEqual({
      message: `成功添加了${fakeContact.contactName}。",
    });
  });
});

在这个测试用例中,我们使用了被称为间谍函数的特殊匹配器。这些工具可以用来检测某个函数被调用了多少次,或者调用时使用了哪些参数等等。

在这里,我们期望`res.status`这个函数会被调用,并且其返回值应为201;同时,`res.json`中也会包含一条消息,用于向用户反馈操作结果。

集成测试

集成测试用于检测应用程序各部分之间的交互与配合情况。例如,它们会检查数据库是否能够顺利地与那些负责向数据库发送API请求的函数协同工作。

与单元测试不同(在单元测试中我们不需要让测试代码本身发起API调用),集成测试的目的是验证应用程序的不同组件是否能够正常协作、并按预期运行。在这种情况下,我们不需要使用任何模拟工具,因为我们的目标是确认请求确实被成功发送到了后端系统,并且数据库连接也建立了。

创建集成测试数据存储环境

在执行集成测试时,有两种方法可以创建用于替代真实数据库的测试环境。具体方法如下:

  • 创建与真实数据库结构完全相同的副本,并将其作为测试环境使用。每次进行测试时,只需将应用程序的连接设置指向这个测试数据库即可。

  • 使用内存中的数据库存储机制。这种做法不需要分别维护两个数据库架构(一个用于测试,另一个用于生产环境)。你可以在代码中直接构建相同的数据库结构,并将其作为测试环境使用。

使用第一种方法会比较复杂,因为你需要为不同的环境配置相应的数据库。而采用第二种方法的话,只需设置正确的数据库结构作为测试用数据,无需在数据库中进行额外配置,测试结束后也可以直接删除这些数据。

对于MongoDB来说,有一个包可以简化内存存储相关的操作,那就是mongodb-memory-server。这个包会在测试完成后自动删除所有用于测试的数据。

如何为集成测试配置环境

你需要安装以下软件包:

  • supertest:这个包可以帮助你在测试过程中发送API请求并获取响应结果。

  • mongodb-memory-server:这个包用于在内存中创建数据库来存储测试数据。

# 使用pnpm包管理器的命令
pnpm add -D mongodb-memory-server supertest @types/supertest

# 使用npm包管理器的命令
npm install --save-dev mongodb-memory-server supertest @types/supertest

# 使用yarn包管理器的命令
yarn add --dev mongodb-memory-server supertest @types/supertest

# 使用bun包管理器的命令
bun add -d mongodb-memory-server supertest @types/supertest

在继续下一步之前,我们需要修改服务器配置文件的内容。

如果你之前使用过Node.js的Express框架或其他类似的开发框架,那么你应该熟悉这种将所有服务器配置代码放在一个文件中的编写方式:

import express from "express";
import { loadEnvFile } from "node:process";
import connectToDB from "../config/dbConfig";
import addContacts from "./controllers/add-contacts.controller";

const app = express();
loadEnvFile();
async function dbConnection() {
  await connectToDB();
}
dbConnection();

app.use(express.json());
app.post("/add-contacts", addContacts);
app.listen(5000, () => console.log("服务器已成功启动"));

这种配置方式在某些情况下确实可行,但在进行集成测试时就会遇到问题。因为在集成测试中,我们需要创建一个Express实例来发送请求。如果我们在主配置文件中导出了这个app变量,那么在生产环境中的数据库连接就会与测试环境中的数据库连接发生冲突,从而导致测试无法正常进行。

为了解决这个问题,我们可以创建另一个文件,在其中定义并导出一个Express实例,然后在服务器代码中使用这个导出的实例来启动服务器。具体的配置方式如下:

app.ts

import express from "express";
import addContacts from "./controllers/add-contacts.controller";

const app = express();

app.use(express.json());
app.post("/add-contacts", addContacts);

export default app;

然后,主文件 `server.ts` 或 `main.ts` 会如下使用 `app` 变量:

import { loadEnvFile } from "node:process";
import connectToDB from "../config/dbConfig";
import app from "./app";

loadEnvFile();

async function bootsrap() {
  await connectToDB();
  app.listen(5000, () => console.log("服务器已成功连接"));
}
bootsrap();

我们从 `app` 文件中导入了 Express 实例,然后使用 `bootstrap()` 函数来配置数据库并启动服务器。这样一来,我们就可以开始为 `addContact` 模块编写集成测试了。

如何编写集成测试

提示:在进行集成测试时,文件名可以采用 `filename.integration.test.ts` 这样的格式。这是最常见的集成测试命名规范,但并非强制要求,只是一种惯例而已。

首先,你需要使用 `mongoose` 和 `mongdb-memory-server` 包来配置数据库测试数据存储环境,并利用 Express 实例发送请求。

import { afterAll, beforeAll, describe, expect, it } from "vitest";

import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import app from "../src/app";

describe("add contact api 的集成测试配置", () => {
  let mongoServer: MongoMemoryServer;
  let server: any;

  beforeAll(async () => {
    mongoServer = await MongoMemoryServer.create();
    const uri = mongoServer.getUri();
    await mongoose.connect(uri);
    server = app.listen(0);
  });

  afterAll.async () => {
    mongoServer.stop();
    mongoosedisconnect();
    server.close();
  });
});

`beforeAll` 和 `afterAll` 函数属于 `vitest` 库。`beforeAll` 会在所有测试开始执行之前运行,而 `afterAll` 则会在所有测试完成后运行。

在测试中,我们会在任何测试开始之前先配置数据库和 Express 实例。

首先创建了 `mongoServer` 变量,然后在 `beforeAll` 块中使用 `MongoMemoryServer.create` 方法初始化它,从而创建一个用于存储测试数据的内存数据库。接着通过 `getUri` 方法获取连接字符串,最后使用 Mongoose 连接到这个内存数据库。

`server` 变量被赋值为监听 0 端口的 Express 实例,但实际上你可以使用任何你喜欢的端口号——这里只是为了演示而已。这样我们就为测试环境创建了一个 Express 实例。

在 `afterAll` 中,所有测试完成后,我们会关闭服务器和内存数据库,并断开 Mongoose 实例的连接。

在同一个 `describe` 块中,我们接着添加测试描述和断言内容(这与我们在单元测试中所做的方式相同):

import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";
import app from "../src/app";
import request from "supertest";
import Contacts from "..../schema/contactList";

describe("添加联系人的集成测试", () => {
  /*
    在这里,我们进行服务器配置以及内存数据库的设置,这些内容在之前的代码片段中已经讨论过,
    用于搭建集成数据存储测试环境 */
  const fakeContact = {
    contactName: "arnold",
    phoneNumber: 798600102,
    email: "arnoldjabo@gmail.com",
  };

  describe("POST /add-contacts", () => {
    it("能够向数据库中添加新记录", async () => {
      const response = await request(app)
        .post("/add-contacts")
        .send(fakeContact);

       const contactList = await Contacts.findOne({
        email: "arnoldjabo@gmail.com",
      })!;
      expect(contactList?.email).toBe("arnoldjabo@gmail.com");
      console.log(contactList);

      expect(response.status).toBe(201);
      expect(response.body).toEqual({
        message: `成功添加了 ${fakeContact.contactName}`,
      });
    });
  });
});

在这段测试代码中,我们将这个测试描述为针对“/add-contacts”端点的POST方法测试。接下来,我们会验证该接口是否能够真正将数据添加到数据库中。

在 `it` 块内部,我们使用了 `supertest` 库中的 `request` 方法来向我们创建的服务器发送请求,并指定了想要测试的HTTP方法及端点。

对于那些需要向后端发送数据的接口(如POST、PATCH或PUT),我们会使用 `request` 对象的 `send()` 方法,传入包含要发送的数据的对象。

我们通过检查响应内容来验证测试结果是否正确。例如,我们预期服务器在数据添加成功时会返回状态码201,并且响应体中会包含一条确认联系人已成功添加的消息。

我们使用断言来检查响应的状态码是否符合预期,同时也会验证响应体中的内容是否与预期的消息一致。

为了确认数据确实被添加到了数据库中,我们通过 `Contacts.findOne()` 方法根据电子邮件地址查找刚刚添加的联系人记录,然后将结果输出到控制台。这种基于内存数据库的测试方式与真正的MongoDB实例几乎是一样的。如果运行这些测试,我们会看到类似以下的输出结果:

集成测试的输出结果,控制台日志显示了使用内存数据库存储数据后的实际效果。<您可以从控制台中看到,这种存储和检索数据的方式与普通的Mongo数据库完全相同。

何时使用单元测试与集成测试

<那么,究竟在什么情况下应该使用这两种类型的测试呢?

<当您遇到像我们之前讨论过的电子邮件验证功能这样的纯函数时,就应该使用单元测试。

<而对于那些需要调用外部API或依赖其他外部服务的功能来说,集成测试才是更合适的选择。这样就可以避免为每一个被引入的函数都编写模拟代码了。

总结

<本指南介绍了两种测试类型的工作原理:单元测试和集成测试。

<单元测试用于单独检测代码库中的特定部分,因此它们特别适合处理纯函数;而集成测试则用于验证应用程序各组成部分之间的交互是否正常,所以它们更适合处理非纯函数。

<如果这篇文章对您有所帮助,您可以请我喝咖啡来表达感谢。

相关文章

技术实践

如何测试Flutter应用程序:单元测试、组件测试、黄金标准测试以及集成测试详解

第一次在技术面试中被问到“你的测试覆盖范围是多少?”时,我并没有一个令人满意的答案。 那时我已经发布了几款真正的Flutter应用程序,它们可以正常运行,用户也在使用它们。但我的测试工作其实非常有限——仅仅是为某个定价功能编写了少量的单元测试而已,并没有其他测试内容。 几个月后,我对其中一个任务完成流程进行了重构,这个修改在代码差异对比中看起来完全没问题,但却破坏了用户真正关心的一个功能:当用户将某项任务标记为已完成时,该任务并不会从错误的列表中移除。虽然没有任何程序崩溃,也没有任何错误日志被记录下来,但用户却开始不再信任这款应用程序。而我直到有用户把这个问题告诉了一位也是测试人员的朋友,才发

阅读全文
技术实践

如何在Flutter中测试人工智能相关功能?[完整指南]

你花了两周时间开发这个AI助手。流式聊天功能设计得很美观,系统提示语也很简洁,安全过滤机制也配置好了。 你向团队展示了这个成果,大家都感到非常印象深刻。随后你将应用提交到了App Store,它终于上线了。 然而在上线三天后,有用户反馈称:如果连续快速点击两次发送按钮,就会出现两个永远无法停止旋转的加载图标;还有用户发现,如果在聊天过程中关闭应用程序再重新打开,聊天界面就会崩溃。 你们团队中的某位成员修改了`AIRepository`中的错误信息字符串,但由于测试用例检测的内容有误,所以测试结果仍然显示正常。一位产品经理询问如果Gemini API不可用,这个新功能是否会出问题,但没有人知道答

阅读全文
技术实践

Cloudflare Workers能够接收入站的TCP连接,而gRPC则是首批被支持使用的协议之一。

现在,Cloudflare Workers可以通过通过Spectrum路由的新connect(socket)处理程序来接收传入的TCP连接,从而结束了此前对HTTP使用的八年限制。使用任何语言编写的容器都可以使用全双工的gRPC协议进行通信;而Cloudflare Workers则可以通过自动进行的gRPC-to-web转换机制来实现单向通信或服务器端流式传输功能。目前所有这些功能都还处于私人测试阶段。 作者:Steef-Jan Wiggers

阅读全文
技术实践

使用 Meta Muse Code 与 Muse Spark 来构建人工智能代理程序、API 以及全栈应用程序。

随着人工智能开发工具的不断扩展,那些采用垂直集成架构的生态系统为软件的开发提供了强大的解决方案。在freeCodeCamp.org的YouTube频道上,讲师Andrew Brown在这个长达三小时的课程中详细讲解了如何利用Meta公司的Muse生态系统来构建应用程序以及自主智能体工作流程,其中涵盖了Muse Spark模型和Muse Code终端工具的使用方法。 这个课程内容涵盖了很多方面,从低级别的API集成,到使用这些工具进行的全栈项目开发,无一不包括: 模型概述与性能评估 了解Muse Spark模型以及开源的Glimmer模型在成本、性能和多语言推理能力方面的优势。 API集成与兼容

阅读全文