集成测试可让您确保应用程序的各个组件正常协同工作。在本文中,您将了解如何设置测试环境以及如何编写集成测试。
目录
- 目录
- 简介
- 先决条件
- 在 Docker 容器中设置 Postgres
- 为集成测试添加 Vitest 配置文件
- 更新单元测试配置
- 编写脚本以启动测试环境
- 配置您的 npm 脚本
- 编写集成测试
- 总结和下一步
简介
到目前为止,在本系列中,您已经探索了如何模拟 Prisma 客户端,并使用该模拟的 Prisma 客户端针对应用程序的小型隔离区域编写单元测试。
在本系列的这一部分中,您将告别模拟的 Prisma 客户端,并针对真实的数据库编写集成测试!在本文末尾,您将设置一个集成测试环境,并为您的 Express API 编写集成测试。
什么是集成测试?
在本系列的上一篇文章中,您学习了编写单元测试,重点测试代码的小型隔离单元,以确保应用程序的最小构建块正常运行。这些测试的目标是测试特定的场景和功能片段,而无需担心底层数据库、外部模块或组件之间的交互。
但是,集成测试是一种完全不同的思路。这种测试包括采用应用程序的相关区域或组件,并确保它们协同工作正常。
上面的图表说明了一个示例场景,其中获取用户的帖子可能需要多次命中数据库,以验证用户是否可以访问 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
准备测试环境的第一件事是使用 Docker 和 Docker 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 镜像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 模式应用于数据库。
- 运行你的集成测试。作为奖励,你还应该能够使用
--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
的顶部导入名为 request
的 supertest
。同时导入 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
来验证其请求主体是否包含通过中间件(名为 validate
,定义在 src/lib/middlewares.ts
中)的有效 username
和 password
字段。
此测试将专门确保 validate
中间件和 zod
定义按预期工作。
为此场景添加一个新测试
此测试将非常简单。它只需向 /auth/signup
端点发送一个 POST
请求,并提供一个无效的请求主体。
使用 supertest
向 /auth/signup
发送一个 POST
请求,但是发送一个 email
字段,而不是一个 username
字段
此请求主体应导致验证中间件在继续控制器之前响应请求,并返回 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 时事通讯