集成测试允许您确保应用程序的各个组件协同工作。在本文中,您将了解如何设置测试环境以及如何编写集成测试。
目录
- 目录
- 简介
- 前提条件
- 在 Docker 容器中设置 Postgres
- 为集成测试添加 Vitest 配置文件
- 更新单元测试配置
- 编写脚本以启动测试环境
- 配置您的 npm 脚本
- 编写集成测试
- 总结 & 接下来是什么
简介
到目前为止,在本系列中,您已经探索了模拟 Prisma 客户端以及使用模拟的 Prisma 客户端针对应用程序的小型隔离区域编写单元测试。
在本系列文章中,您将告别模拟的 Prisma 客户端,并针对真实的数据库编写集成测试!在本文结束时,您将设置一个集成测试环境,并为您的 Express API 编写集成测试。
什么是集成测试?
在本系列的前一篇文章中,您学习了编写单元测试,重点是测试小型隔离的代码单元,以确保应用程序的最小构建块正常运行。这些测试的目标是测试特定的场景和功能片段,而无需担心底层数据库、外部模块或组件之间的交互。
然而,集成测试是一种完全不同的思维方式。这种测试涉及选取应用程序的相关区域或组件,并确保它们协同工作。

上图说明了一个示例场景,其中获取用户的帖子可能需要多次访问数据库,以验证用户是否有权访问 API 或任何帖子,然后再实际检索数据。
如上所示,应用程序的多个组件可能参与处理单个请求或操作。这通常意味着在单个请求或调用期间,数据库交互会在不同组件之间发生多次。因此,集成测试通常包括一个包含数据库的测试环境,以便进行测试。
通过对集成测试的简要概述,您现在将开始准备一个测试环境,您将在其中运行集成测试。
您将使用的技术
前提条件
假设知识
在执行以下步骤时,拥有以下知识将很有帮助
- JavaScript 或 TypeScript 的基本知识
- Prisma Client 及其功能的基本知识
- 对 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
您为准备测试环境要做的第一件事是使用 Docker Docker Compose 构建一个 Docker 容器,该容器提供 Postgres 服务器。这将是您的应用程序在运行集成测试时使用的数据库。
但是,在继续之前,请确保您已在机器上安装并运行 Docker。您可以按照此处的步骤在您的机器上设置 Docker。
要开始配置 Docker 容器,请在项目根目录创建一个名为 docker-compose.yml
的新文件
您将在此文件中配置容器,让 Docker 知道如何设置数据库服务器、使用哪个镜像(Docker 镜像是一组详细说明如何构建容器的指令)以及如何存储容器的数据。
注意:您可以在
docker-compose.yml
文件中配置很多内容。您可以在此处找到文档。
您的容器应创建并公开一个 Postgres 服务器。
为了实现这一点,首先指定您将使用的 Compose 文件格式的版本
此版本号还决定了您将使用的 Docker Engine 的版本。3.8
是撰写本文时最新的版本。
接下来,您将需要一个 service
,您的数据库服务器将在其中运行。创建一个名为 db
的新 service
,并具有以下配置
上面添加的配置指定了一个名为 db
的服务,并具有以下配置
image
:定义构建此服务时要使用的 Docker 镜像restart
:always
选项让 Docker 知道在发生故障或 Docker 重新启动时重新启动此服务environment
:配置要在容器内公开的环境变量ports
:指定 Docker 应将您机器的5432
端口映射到容器的5432
端口,Postgres 服务器将在该端口上运行volumes
:指定卷的名称以及容器将在本地机器上持久保存其数据的位置
为了完成服务的配置,您需要让 Docker 知道如何配置卷配置中定义的卷并将其联网。
将以下内容添加到您的 docker-compose.yml
文件中,以让 Docker 知道卷应存储在您的本地 Docker 主机机器上(在您的文件系统中)
如果您现在前往终端并导航到项目的根目录,您应该能够运行以下命令来启动运行 Postgres 的容器

您的数据库服务器现在可用,并且可以从 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
的文件
您将在此文件中编写和导出一个重置数据库的函数。
您的数据库只有三个表:Tag
、Quote
和 User
。编写并导出一个函数,该函数在事务中对每个表运行 deleteMany
通过上面编写的文件,您现在有了一种清除数据库的方法。这里要做的最后一件事是在每个集成测试之间实际调用该函数。
一种很好的方法是使用setup file。这是一个您可以配置 Vitest 在运行任何测试之前处理的文件。在这里,您可以使用 Vitest 的生命周期钩子来自定义其行为。
在 src/tests/helpers
中创建另一个名为 setup.ts
的文件。
您的目标是在每个测试之前重置数据库,以确保您有一个干净的状态。您可以通过在 Vitest 提供的 beforeEach
生命周期函数中运行 reset-db.ts
导出的函数来实现此目的。
在 setup.ts
中,使用 beforeEach
在每个测试之间运行您的重置函数
现在,当您运行测试套件时,src/tests
中所有文件中的每个单独的测试都将从干净的状态开始。
注意:您可能想知道在特定测试上下文中想要从某些数据开始的场景。在您编写的每个单独的测试文件中,您还可以挂接到这些生命周期函数并自定义每个文件的行为。稍后将显示一个示例。
最后,您现在需要让 Vitest 知道此 setup file,并告诉它在您运行测试时运行该文件。
使用以下内容更新 vitest.config.integration.ts
更新单元测试配置
目前,单元测试配置文件也将运行您的集成测试,因为它会搜索 src
中的任何 .ts
文件。
更新 vitest.config.unit.ts
中的配置以忽略 src/tests
中的文件
现在您的单元测试和集成测试完全分离,并且只能使用各自的命令运行。
编写脚本以启动测试环境
到目前为止,您已经构建了以下方法来
- 在 Docker 容器中启动数据库服务器
- 使用特定的测试配置运行集成测试
- 将单元测试与集成测试分开运行
缺少的是一种实际编排 Docker 容器的创建和集成测试运行的方法,以确保您的数据库正在运行并且可用于您的测试环境。
为了使这项工作正常进行,您将编写一组自定义 bash 脚本,这些脚本启动您的 Docker 容器,等待服务器准备就绪,然后运行您的测试。
在项目根目录创建一个名为 scripts
的新目录
在该目录中,创建一个名为 run-integration.sh
的文件
在此文件中,您需要执行以下步骤
- 从
.env
加载任何环境变量,以便您可以访问数据库 URL。 - 在分离模式下启动 Docker 容器。
- 等待数据库服务器变为可用。
- 运行 Prisma 迁移以将您的 Prisma schema 应用于数据库。
- 运行您的集成测试。作为奖励,您还应该能够使用
--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 迁移,最后正在运行您的测试

注意:目前您的测试将失败。这是因为 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 的 describe
、expect
和 it
函数,并使用 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
的顶部导入 supertest
,名称为 request
。 还要从 src/lib/createServer
导入默认导出,它提供 app
对象
现在,您可以使用 request
函数向您的 Express API 发送请求
上面,app
实例被传递给 request
函数。该函数的响应是一组函数,允许您与传递给 request
的 HTTP 服务器进行交互。
然后使用 post
函数来定义您打算与之交互的HTTP 方法和路由。最后,调用 send
以发送 POST
请求以及请求正文。
返回的值包含请求响应的所有详细信息,但是 status
和 body
值是专门从响应中提取出来的。
现在您有权访问响应状态和正文,您可以验证路由是否执行了其预期的操作并以正确的值进行了响应。
将以下内容添加到此测试以验证这些情况
上面的更改执行以下操作
- 导入
prisma
,以便您可以查询数据库以仔细检查数据是否已正确创建 - 使用
prisma
获取新创建的用户 - 确保请求以
200
状态码响应 - 确保找到了用户记录
- 确保响应正文包含一个
user
对象,其中包含用户的username
和id
如果您在终端中运行 npm run test:int:ui
,您应该会看到 Vitest GUI 打开以及成功的测试消息。
注意:如果您尚未运行此命令,系统可能会提示您安装
@vitest/ui
包并重新运行该命令。

注意:此测试中没有模拟任何模块,包括 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
来验证其请求正文是否包含有效的 username
和 password
字段,通过名为 validate
的中间件在 src/lib/middlewares.ts
中定义。
此测试将专门确保 validate
中间件和 zod
定义按预期工作。
为此场景添加一个新测试
此测试将非常简单明了。 它应仅向 /auth/signup
端点发送 POST
请求并提供无效的请求正文。
使用 supertest
向 /auth/signup
发送 POST
请求,但是,不要发送 username
字段,而是发送 email
字段
此请求正文应导致验证中间件在继续到控制器之前以 400
错误代码响应请求。
使用以下一组期望来验证此行为
这样,您的 /auth/signup
端点的测试套件就完成了! 如果您回顾一下 Vitest GUI,您应该会发现您的所有测试都已成功通过

为 /auth/signin
编写测试
您将要为其编写测试的下一个端点与上一个端点有很多相似之处,但是,它不是创建新用户,而是验证现有用户。
/auth/signin
端点接收 username
和 password
,确保存在具有所提供数据的用户,生成会话令牌,并使用会话令牌和用户详细信息响应请求。
注意:此功能的实现可以在
src/auth/auth.controller.ts
和src/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
成功时,它应该以用户详细信息进行响应
下一个测试与上一个测试非常相似,不同之处在于,您将检查响应正文中是否存在 user
对象并验证其内容,而不是检查 200
响应状态。
添加另一个包含以下内容的测试
上面的测试内容执行以下操作
- 向
/auth/signin
发送POST
请求,请求正文包含测试用户的用户名和密码 - 提取响应正文的
user
对象的键 - 验证响应中是否存在两个键
id
和username
,并且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,您应该会看到您的整个端点测试套件都成功通过了所有检查。

摘要和后续步骤
恭喜您阅读到本文结尾! 测试系列的这一部分内容丰富,让我们回顾一下。 在本文中,您
- 了解了什么是集成测试
- 设置了 Docker 容器以在您的测试环境中运行 Postgres 数据库
- 配置了 Vitest,以便您可以独立运行单元测试和集成测试
- 编写了一组启动 shell 脚本来启动您的测试环境并运行您的集成测试套件
- 为您的 Express API 中的两个主要端点编写了测试
在本系列的下一部分中,您将了解将在这些文章中介绍的最后一种测试类型:端到端测试。
我们希望您继续关注本系列的其余部分!
不要错过下一篇文章!
注册 Prisma 新闻通讯