2023 年 2 月 14 日

使用 Prisma 进行测试的终极指南:集成测试

集成测试可让您确保应用程序的各个组件正常协同工作。在本文中,您将了解如何设置测试环境以及如何编写集成测试。

The Ultimate Guide to Testing with Prisma: Integration Testing

目录

简介

到目前为止,在本系列中,您已经探索了如何模拟 Prisma 客户端,并使用该模拟的 Prisma 客户端针对应用程序的小型隔离区域编写单元测试。

在本系列的这一部分中,您将告别模拟的 Prisma 客户端,并针对真实的数据库编写集成测试!在本文末尾,您将设置一个集成测试环境,并为您的 Express API 编写集成测试。

什么是集成测试?

在本系列的上一篇文章中,您学习了编写单元测试,重点测试代码的小型隔离单元,以确保应用程序的最小构建块正常运行。这些测试的目标是测试特定的场景和功能片段,而无需担心底层数据库、外部模块或组件之间的交互。

但是,集成测试是一种完全不同的思路。这种测试包括采用应用程序的相关区域或组件,并确保它们协同工作正常。

diagram of request

上面的图表说明了一个示例场景,其中获取用户的帖子可能需要多次命中数据库,以验证用户是否可以访问 API 或任何帖子,然后才能实际检索数据。

如上所示,应用程序的多个组件可能会参与处理单个请求或操作。这通常意味着在单个请求或调用期间,数据库交互会在不同组件中发生多次。因此,集成测试通常包括一个测试环境,其中包含要测试的数据库。

通过对集成测试的简要概述,您现在将开始准备一个测试环境,您将在其中运行集成测试。

您将使用的技术

先决条件

假设的知识

在执行以下步骤时,拥有以下知识会很有帮助

  • JavaScript 或 TypeScript 的基本知识
  • Prisma 客户端及其功能的基本知识
  • Docker 的基本理解
  • 一些测试框架的经验

开发环境

要遵循提供的示例,您需要

本系列大量使用了这个 GitHub 存储库。确保克隆该存储库并检出 unit-tests 分支,因为该分支是本文的起点。

克隆存储库

在您的终端中,转到您存储项目的目录。在该目录中,运行以下命令

上面的命令会将项目克隆到名为 express_sample_app 的文件夹中。该存储库的默认分支是 main,因此您需要检出 unit-tests 分支。

克隆存储库后,需要执行几个步骤来设置项目。

首先,导航到项目并安装 node_modules

接下来,在项目的根目录创建一个 .env 文件

此文件应包含一个名为 API_SECRET 的变量,您可以将其值设置为任何您想要的 string,以及一个名为 DATABASE_URL 的变量,该变量现在可以保留为空

.env 文件中,API_SECRET 变量提供了一个密钥,该密钥被身份验证服务用于加密你的密码。在实际应用中,这个值应该替换为一个由数字和字母字符组成的长随机字符串。

DATABASE_URL,顾名思义,包含你数据库的 URL。你目前没有或不需要真实的数据库。

在 Docker 容器中设置 Postgres

准备测试环境的第一件事是使用 DockerDocker Compose 构建一个提供 Postgres 服务器的 Docker 容器。这将是你的应用程序在运行集成测试时使用的数据库。

在继续之前,请确保你的机器上已安装并运行 Docker。你可以按照此处的步骤在你的机器上安装 Docker。

要开始配置你的 Docker 容器,请在你的项目根目录创建一个名为 docker-compose.yml 的新文件

你将在此文件中配置你的容器,让 Docker 知道如何设置数据库服务器,使用哪个镜像(Docker 镜像是一组详细说明如何构建容器的指令),以及如何存储容器的数据。

注意:你可以在 docker-compose.yml 文件中配置很多内容。你可以在此处找到文档。

你的容器应创建并公开一个 Postgres 服务器。

为了实现这一点,首先指定你将使用的 Compose 文件格式的版本

此版本号还决定了你将使用的 Docker Engine 版本。在撰写本文时,3.8 是最新版本。

接下来,你需要一个service,你的数据库服务器将在其中运行。创建一个名为 db 的新 service,并使用以下配置

上面添加的配置指定了一个名为 db 的服务,具有以下配置

  • image:定义构建此服务时要使用的 Docker 镜像
  • restartalways 选项让 Docker 知道在发生故障或 Docker 重新启动时重新启动此服务
  • environment:配置要在容器中公开的环境变量
  • ports:指定 Docker 应该将你机器的 5432 端口映射到容器的 5432 端口,Postgres 服务器将在该端口上运行
  • volumes:指定卷的名称以及容器将在你的本地机器上持久化其数据的位置

要完成你的服务配置,你需要让 Docker 知道如何配置和联网在 volumes 配置中定义的卷。

将以下内容添加到你的 docker-compose.yml 文件中,让 Docker 知道这些卷应该存储在你的本地 Docker 主机上(在你的文件系统中)

如果你现在转到你的终端并导航到你的项目根目录,你应该能够运行以下命令来启动运行 Postgres 的容器

Docker Compose running

你的数据库服务器现在可用,可以通过 URL postgres://postgres:postgres@localhost:5432 访问。

更新你的 .env 文件的 DATABASE_URL 变量以指向该 URL,并将 quotes 指定为数据库名称

为集成测试添加 Vitest 配置文件

在上一篇文章中,你为 Vitest 创建了一个配置文件。该配置文件 vitest.config.unit.ts 专门用于项目中的单元测试。

现在,你将创建第二个配置文件,名为 vitest.config.integration.ts,你将在其中配置 Vitest 在运行集成测试时应如何运行。

注意:在本系列中,这些文件将非常相似。根据你的项目的复杂性,像这样拆分你的配置会变得更加明显地有益。

在你的项目根目录创建一个名为 vitest.config.integration.ts 的新文件

将以下内容粘贴到该新文件中

上面的代码片段与 vitest.config.unit.ts 的内容基本相同,除了 test.include blob 指向 src/tests 中的任何 .ts 文件,而不是像单元测试配置那样指向 src 中的任何文件。这意味着你的所有集成测试都应该放在 src 中名为 tests 的新文件夹中。

接下来,在此新配置文件中添加另一个键,告诉 Vitest 不要同时在不同的线程中运行多个测试

这非常重要,因为你的集成测试将与数据库交互并期望特定的数据集合。如果多个测试同时运行并与你的数据库交互,你可能会因意外数据而导致测试出现问题。

类似地,你还需要一种方法来在测试之间重置你的数据库。在此应用程序中,在每个测试之间,你将完全清除你的数据库,以便你可以在每个测试中从空白状态开始。

src 中创建一个名为 tests 的新文件夹,并在 tests 中创建一个名为 helpers 的新文件夹

在该新目录中,创建一个名为 prisma.ts 的文件

此文件是一个帮助程序,它只是实例化并导出 Prisma Client。

将以下内容添加到该文件中

现在在 src/tests/helpers 中创建另一个名为 reset-db.ts 的文件

你将在此文件中编写并导出一个重置数据库的函数。

你的数据库只有三个表:TagQuoteUser。编写并导出一个函数,该函数在事务中对每个表运行 deleteMany

使用上面编写的文件,你现在可以清除你的数据库。这里最后要做的事情是在每个集成测试之间实际调用该函数。

一种好方法是使用设置文件。这是一个你可以配置 Vitest 在运行任何测试之前处理的文件。在这里,你可以使用 Vitest 的生命周期钩子来自定义其行为。

src/tests/helpers 中创建另一个名为 setup.ts 的文件。

你的目标是在每个测试之前重置数据库,以确保你有一个干净的状态。你可以通过在 Vitest 提供的 beforeEach 生命周期函数中运行 reset-db.ts 导出的函数来实现此目的。

setup.ts 中,使用 beforeEach 在每个测试之间运行你的重置函数

现在,当你运行你的测试套件时,src/tests 中所有文件中的每个单独的测试都将从干净的状态开始。

注意:你可能想知道在特定测试上下文中要从一些数据开始的场景。在你编写的每个单独的测试文件中,你还可以挂接到这些生命周期函数并自定义每个文件的行为。稍后将显示一个示例。

最后,你现在需要让 Vitest 知道此设置文件,并告诉它在运行测试时运行该文件。

使用以下内容更新 vitest.config.integration.ts

更新单元测试配置

当前,单元测试配置文件也将运行你的集成测试,因为它会搜索 src 中的任何 .ts 文件。

更新 vitest.config.unit.ts 中的配置,以忽略 src/tests 中的文件

现在,你的单元测试和集成测试是完全分开的,只能使用它们自己的单独命令运行。

编写一个脚本来启动测试环境

到目前为止,你已经构建了以下方法:

  • 在 Docker 容器中启动数据库服务器
  • 使用特定的测试配置运行集成测试
  • 与集成测试分开运行单元测试

缺少的是一种实际编排 Docker 容器的创建和集成测试运行的方式,以确保你的数据库正在运行并且可用于你的测试环境。

为了使它工作,你将编写一组自定义 bash 脚本,这些脚本将启动你的 Docker 容器,等待服务器准备就绪,然后运行你的测试。

在你的项目根目录创建一个名为 scripts 的新目录

在该目录中,创建一个名为 run-integration.sh 的文件

在此文件中,你需要执行以下步骤

  1. .env 加载任何环境变量,以便你可以访问数据库 URL。
  2. 在分离模式下启动你的 Docker 容器。
  3. 等待数据库服务器可用。
  4. 运行 Prisma 迁移以将你的 Prisma 模式应用于数据库。
  5. 运行你的集成测试。作为奖励,你还应该能够使用 --ui 标志运行此文件,以运行 Vitest 的 GUI 界面

加载环境变量

第一步是读取 .env 文件,并使这些变量在你的脚本上下文中可用。

scripts 目录中创建另一个名为 setenv.sh 的文件

在此文件中,添加以下代码段

这将读取你的 .env 文件并导出每个变量,使其在你的脚本中可用。

回到 scripts/run-integration.sh,你现在可以使用此文件通过 source 命令访问环境变量

在上面,DIR 变量用于查找 setenv.sh 的相对路径,该路径用于执行脚本。

以分离模式启动你的 Docker 容器

下一步是启动你的 Docker 容器。请务必注意,你需要以分离模式启动容器。

通常,当你运行 docker-compose up 时,你的终端将连接到容器的输出,以便你可以看到正在发生的事情。然而,这会阻止终端执行任何其他操作,直到你停止 Docker 容器。

以分离模式运行容器允许它在后台运行,从而释放你的终端以继续运行命令(例如运行集成测试的命令)。

将以下内容添加到 run-integration.sh

在这里,-d 标志表示容器应以分离模式运行。

使脚本等待直到数据库服务器准备就绪

在运行你的 Prisma 迁移和测试之前,你需要确保你的数据库已准备好接受请求。

为了做到这一点,你将使用一个众所周知的脚本,称为 wait-for-it.sh。此脚本允许你提供 URL 以及一些定时配置,并将使脚本等待,直到提供的 URL 处的资源可用,然后再继续。

通过运行以下命令,将该脚本的内容下载到名为 scripts/wait-for-it.sh 的文件中

警告:如果 wait-for-it.sh 脚本对你不起作用,请参阅 GitHub 讨论,了解连接到数据库并确保测试成功运行的替代方法。

然后,回到 run-integration.sh 并使用以下内容更新它

你的脚本现在将等待 DATABASE_URL 环境变量中指定位置的数据库可用,然后再继续。

如果你使用的是 Mac,你还需要运行以下命令来安装并别名一个在 wait-for-it.sh 脚本中使用的命令

准备数据库并运行测试

现在可以安全地执行最后两个步骤了。

在运行 wait-for-it 脚本后,运行 Prisma 迁移以将任何新更改应用到数据库

然后,要完成所有这些,请添加以下语句来运行你的集成测试

请注意使用的 if/else 语句。这使你可以查找传递给脚本的标志。如果找到标志,则假定为 --ui,并将使用 Vitest 用户界面运行测试。

使脚本可执行

运行测试所需的脚本都已完成,但是,如果你尝试执行任何脚本,将会收到权限错误。

为了使这些脚本可执行,你需要运行以下命令,该命令使你当前的用户可以访问它们以运行它们

配置你的 npm 脚本

你的脚本现在是可执行的。下一步是在 package.json 中创建 scripts 记录,这些记录将调用这些自定义脚本并启动你的测试。

package.json 中,将以下内容添加到 scripts 部分

现在,如果你运行以下任何脚本,你应该看到你的 Docker 容器启动,执行 Prisma 迁移,最后运行你的测试

tests running and failing

注意:目前,你的测试将失败。这是因为 Vitest 找不到任何要运行的测试文件。

编写集成测试

现在是时候使用你的测试环境并编写一些测试了!

在考虑应用程序的哪些部分需要集成测试时,重要的是要考虑组件之间重要的交互以及如何调用这些交互。

在你要使用的 Express API 中,重要的交互分组发生在路由控制器服务之间。当用户访问 API 中的端点时,路由处理程序将请求传递给控制器,并且控制器可以调用服务函数与数据库进行交互。

考虑到这一点,你将专注于测试每个单独的路由的集成测试,确保每个路由都正确响应 HTTP 请求。这包括对 API 的有效和无效请求。目标是使你的测试模拟 API 的使用者在与其交互时的体验。

注意:关于集成测试应该涵盖哪些内容,有许多不同的观点。在某些情况下,开发人员可能希望编写专门的集成测试,以确保较小的组件(例如你的控制器服务)一起正常工作,以及验证整个 API 路由是否正常工作的测试。关于测试中应涵盖哪些内容的决定完全取决于你的应用程序的需求以及你作为开发人员认为需要测试的内容。

与本系列的前一篇文章类似,为了使本教程中的信息保持在可管理的长度内,你将专注于为 API 路由 /auth/signin/auth/signup 编写测试。

注意:如果你好奇 /quotes/* 路由的测试是什么样的,完整的测试集可在 GitHub 存储库的 integration-tests 分支中找到。

/auth/signup 编写测试

src/tests 中创建一个名为 auth.test.ts 的新文件

这是与你的 API 的 /auth 路由相关的所有测试所在的位置。

在此文件中,从 Vitest 导入 describeexpectit 函数,并使用 describe 定义此测试套件

你将测试的第一个端点是 POST /auth/signup 路由。

在测试套件上下文中,添加另一个 describe 块来描述与此特定路由关联的测试套件

此路由允许你提供用户名和密码以创建新用户。通过查看 src/auth/auth.controller.ts 中的逻辑和 src/auth/auth.routes.ts 中的路由定义,可以确定以下行为是需要测试的重要行为

  • 它应该响应 200 状态代码和用户详细信息
  • 成功时,它应响应有效的会话令牌
  • 如果已存在具有所提供用户名的用户,它应该响应 400 状态代码
  • 如果提供了无效的请求正文,它应该响应 400 状态代码

注意:测试应包括你期望从 API 获得的任何响应变体,包括成功和错误。通读 API 的路由定义和控制器通常足以确定应测试的不同场景。

接下来的四个部分将详细介绍如何编写这些场景的测试。

它应该响应 200 状态代码和用户详细信息

要开始为此场景编写测试,请使用从 Vitest 导入的 it 函数来描述“它”应该做什么。

/auth/signup 路由的 describe 块中添加以下内容

为了实际将数据 POST 到应用程序中的端点,你将使用名为 supertest 的库。此库允许你为其提供 HTTP 服务器并通过简单的 API 向该服务器发送请求。

supertest 作为开发依赖项安装

然后,在 src/tests/auth.test.ts 的顶部导入名为 requestsupertest。同时导入 src/lib/createServer 的默认导出,该导出提供了 app 对象

你现在可以使用 request 函数向你的 Express API 发送请求

在上面,app 实例已传递给 request 函数。该函数的响应是一组允许你与传递给 request 的 HTTP 服务器进行交互的函数。

然后使用 post 函数定义你打算与之交互的HTTP 方法和路由。最后,调用 send 以发送 POST 请求以及请求正文。

返回的值包含请求响应的所有详细信息,但是 statusbody 值是专门从响应中提取的。

现在你可以访问响应状态和正文,你可以验证路由是否执行了其预期的操作并响应了正确的值。

将以下内容添加到此测试以验证这些情况

上述更改执行以下操作

  1. 导入 prisma,以便您可以查询数据库以仔细检查数据是否已正确创建
  2. 使用 prisma 来获取新创建的用户
  3. 确保请求响应的状态码为 200
  4. 确保找到了用户记录
  5. 确保响应主体包含一个 user 对象,其中包含用户的 usernameid

如果在您的终端中运行 npm run test:int:ui,您应该看到 Vitest GUI 打开并显示测试成功的消息。

注意:如果您尚未运行此命令,可能会提示您安装 @vitest/ui 包并重新运行该命令。

Successful test run

注意:此测试中没有模拟任何模块,包括 Prisma Client!您的测试是针对真实数据库运行的,并验证了此路由中的数据交互是否正常。

成功时应响应有效的会话令牌

下一个测试将验证当创建用户时,响应应包含一个会话令牌,该令牌可用于验证该用户向 API 发出的请求。

在前一个测试下方为此场景创建一个新测试

此测试将比之前的测试简单一些。它只需发送一个有效的注册请求并检查响应以验证是否已发送回有效令牌。

使用 supertest/auth/signup 端点发送 POST 请求并检索响应主体

响应主体应包含一个名为 token 的字段,其中包含会话令牌字符串。

添加一组期望值,以验证 token 字段是否存在于响应中,并使用 jwt 库来验证该令牌是否是有效的会话令牌

如果存在具有所提供用户名的用户,则应响应 400 状态码

到目前为止,您已经验证了对 /auth/signup 的有效请求按预期响应。现在您将改变方向,确保应用程序能够正确处理无效请求。

为此场景添加另一个测试

为了触发当使用现有用户名发出注册请求时应发生的 400 错误,数据库中必须已存在用户。

向此测试添加一个查询,该查询创建一个名为 'testusername' 的用户,并使用任何密码

现在,您应该可以通过使用与该用户相同的用户名发送注册请求来触发错误。

注意:请记住,此用户记录(以及由于您的注册测试而创建的其他记录)在每个单独的测试之间都会被删除。

/auth/signup 发送请求,并提供与上面创建的用户相同的用户名:'testusername'

既然正在向该端点发送请求,那么现在是时候考虑一下您期望在这种情况下会发生什么了。您会期望

  • 请求响应的状态码为 400
  • 响应主体不包含 user 对象
  • 数据库中的用户计数仅为 1

向测试添加以下期望值以验证是否满足所有这些点

如果提供了无效的请求主体,则应响应 400 状态码

您将为此端点编写的最后一个测试是验证如果向 API 发送了无效的请求主体,请求将响应 400 状态码的测试。

此端点(如 src/auth/auth.router.ts 中所示)使用 zod 来验证其请求主体是否包含通过中间件(名为 validate,定义在 src/lib/middlewares.ts 中)的有效 usernamepassword 字段。

此测试将专门确保 validate 中间件和 zod 定义按预期工作。

为此场景添加一个新测试

此测试将非常简单。它只需向 /auth/signup 端点发送一个 POST 请求,并提供一个无效的请求主体。

使用 supertest/auth/signup 发送一个 POST 请求,但是发送一个 email 字段,而不是一个 username 字段

此请求主体应导致验证中间件在继续控制器之前响应请求,并返回 400 错误代码。

使用以下一组期望值来验证此行为

至此,您针对 /auth/signup 端点的测试套件已完成!如果您回顾一下 Vitest GUI,您应该会发现所有测试都成功了

Full suite of signup tests complete

/auth/signin 编写测试

您将要编写测试的下一个端点与上一个端点有许多相似之处,但它不是创建一个新用户,而是验证现有用户。

/auth/signin 端点接收 usernamepassword,确保存在具有所提供数据的用户,生成一个会话令牌,并使用会话令牌和用户的详细信息响应请求。

注意:此功能的实现可以在 src/auth/auth.controller.tssrc/auth/auth.router.ts 中找到。

在您的测试套件中,您将验证以下关于此端点的描述是否为真

  • 当提供有效的凭据时,应响应 200 状态码
  • 成功时应响应用户详细信息
  • 成功时,它应响应有效的会话令牌
  • 当给定无效的凭据时,应响应 400 状态码
  • 当找不到用户时,应响应 400 状态码
  • 当给定无效的请求主体时,应响应 400 状态码

在测试每个场景之前,您需要定义另一个测试套件来分组与此端点相关的所有测试。

在您定义 /auth/signup 测试套件的结束标记下,为 /auth/signin 路由添加另一个 describe

您将在此套件中编写的测试还需要数据库中存在用户,因为您将测试登录功能。

在您刚添加的 describe 块中,您可以使用 Vitest 的 beforeEach 函数在每次测试之前向数据库添加一个用户。

向新的测试套件添加以下内容

注意:重要的是要注意,此处的密码加密方法必须与 src/auth/auth.service.ts 中使用的加密方法完全匹配。

现在,此测试套件的初始设置已完成,您可以继续编写测试了。

和之前一样,接下来的六个部分将单独介绍每个场景,并逐步介绍测试的工作方式。

当提供有效的凭据时,应响应 200 状态码

第一个测试将简单地验证使用正确凭据的有效登录请求是否会导致 API 返回 200 响应代码。

首先,在此测试套件的 describe 块中,在 beforeEach 函数的正下方添加新的测试

为了测试所需的行为,使用用于创建测试用户的相同用户名和密码向 /auth/signin 端点发送 POST 请求。然后验证响应的状态代码是否为 200

成功时应响应用户详细信息

下一个测试与之前的测试非常相似,除了不是检查 200 响应状态,您将检查响应主体中是否存在 user 对象并验证其内容。

添加另一个具有以下内容的测试

上面测试的内容执行以下操作

  1. /auth/signin 发送一个 POST 请求,其请求主体包含测试用户的用户名和密码
  2. 提取响应主体的 user 对象的键
  3. 验证响应中存在两个键,idusername,并且 user.username 的值与测试用户的用户名匹配

成功时应响应有效的会话令牌

在此测试中,您将再次遵循与前两个测试非常相似的过程,只是此测试将验证响应主体中是否存在有效的会话令牌。

在前一个测试下方添加以下测试

如您在上面所见,已向目标端点发送了一个请求,并且从结果中抽象出了响应主体。

使用 toHaveProperty 函数来验证响应主体中是否存在 token 键。然后使用 jwt.verify 函数验证会话令牌。

注意:重要的是要注意,与密码加密类似,重要的是使用与 src/auth/auth.service.ts 中使用的函数相同的函数来验证会话令牌。

当给定无效的凭据时,应响应 400 状态码

您现在将验证发送具有无效凭据的请求主体是否会产生正确的错误响应。

要重新创建此场景,您只需使用测试用户的正确用户名但错误的密码向 /auth/signin 发送 POST 请求。

添加以下测试

如您在上面所见,响应的状态预计为 400

还为响应主体添加了一个期望值,该期望值不包含 token 属性,因为无效的登录请求不应触发会话令牌的生成。

注意:此测试的第二个期望值不是绝对必要的,因为 400 状态码足以知道控制器中的条件已满足,从而可以短路请求并响应错误。

当找不到用户时,应返回 400 状态码

在此,您将测试当使用提供的用户名找不到用户的情况。与之前的测试一样,这应该会短路请求并导致提前返回错误状态码。

将以下内容添加到您的测试中

当给定无效请求体时,应返回 400 状态码

在最后一个测试中,您将验证发送无效请求体会导致错误响应。

src/auth/auth.router.ts 中使用的 validate 中间件应该捕获无效的请求体,并完全短路身份验证控制器。

添加以下测试来完成此测试套件

如您在上面看到的,username 字段被替换为 email 字段,就像本文之前的测试中所做的那样。因此,请求体与请求体的 zod 定义不匹配,并触发错误。

如果您前往 Vitest GUI,您应该看到您的整个测试套件,包括两个端点的所有检查都已成功通过。

Screen showing a full set of passing integration tests

总结 & 后续步骤

恭喜您看到本文的结尾!测试系列的这一部分内容丰富,让我们回顾一下。在本文中,您:

  • 了解了什么是集成测试
  • 设置了一个 Docker 容器,在测试环境中运行 Postgres 数据库
  • 配置了 Vitest,以便您可以独立运行单元测试和集成测试
  • 编写了一组启动 shell 脚本,以启动测试环境并运行集成测试套件
  • 为 Express API 中的两个主要端点编写了测试

在本系列的下一部分中,您将了解本文将涵盖的最后一种测试类型:端到端测试。

我们希望您能继续关注本系列的其余部分!

不要错过下一篇文章!

注册 Prisma 时事通讯