集成测试允许您确保应用程序的各个组件协同工作。在本文中,您将了解如何设置测试环境以及如何编写集成测试。
目录
- 目录
- 介绍
- 先决条件
- 在 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 知道如何配置和联网在 volumes
配置中定义的卷。
将以下内容添加到您的 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
通过上面编写的文件,您现在有了一种清除数据库的方法。这里要做的最后一件事是在每个集成测试之间实际调用该函数。
一种很好的方法是使用_设置文件_。这是一个您可以配置 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
的文件
在此文件中,您将需要执行以下步骤
- 从
.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
发送 POST
请求到 /auth/signup
端点并检索响应正文
响应正文应包含一个名为 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
中定义)来验证其请求正文是否包含有效的 username
和 password
字段。
此测试将专门确保 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 新闻通讯