集成测试可确保应用程序的各个组件协同工作。在本文中,您将了解如何设置测试环境以及如何编写集成测试。
目录
引言
到目前为止,在本系列中,您已经探索了如何模拟 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 Compose 构建一个 Docker 容器,该容器提供 Postgres 服务器。这将是您的应用程序在运行集成测试时使用的数据库。
然而,在继续之前,请确保您的机器上已安装并运行 Docker。您可以按照此处的步骤在您的机器上设置 Docker。
要开始配置您的 Docker 容器,请在项目根目录创建一个名为 docker-compose.yml
的新文件
该文件将用于配置您的容器,让 Docker 知道如何设置数据库服务器、使用哪个 镜像(Docker 镜像是一组详细说明如何构建容器的指令),以及如何存储容器的数据。
注意:您可以在
docker-compose.yml
文件中配置大量内容。您可以在此处找到文档。
您的容器应创建并公开一个 Postgres 服务器。
为此,首先指定您将使用的 Compose 文件格式版本
此版本号还决定了您将使用的 Docker 引擎版本。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 路由是否正常工作的测试。关于测试中应涵盖哪些内容的决定完全取决于您应用程序的需求以及作为开发人员您认为需要测试的内容。
与本系列之前的文章类似,为了使本教程的信息长度可控,您将专注于为 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
安装为开发依赖
然后将 supertest
导入到 src/tests/auth.test.ts
的顶部,命名为 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
通过在 src/lib/middlewares.ts
中定义的名为 validate
的中间件来验证其请求体是否包含有效的 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
成功时应响应用户详情
下一个测试与前一个测试非常相似,但不是检查 200
响应状态,而是检查响应体中的 user
对象并验证其内容。
添加另一个测试,内容如下
以上测试内容执行以下操作
- 向
/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 新闻邮件