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 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 镜像
  • 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 schema 应用到数据库。
  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 路由是否正常工作的测试。关于测试中应涵盖哪些内容的决定完全取决于您应用程序的需求以及作为开发人员您认为需要测试的内容。

与本系列之前的文章类似,为了使本教程的信息长度可控,您将专注于为 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 安装为开发依赖

然后将 supertest 导入到 src/tests/auth.test.ts 的顶部,命名为 request。同时导入 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 通过在 src/lib/middlewares.ts 中定义的名为 validate 的中间件来验证其请求体是否包含有效的 usernamepassword 字段。

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

为此场景添加新测试

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

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

此请求体应导致验证中间件在继续到控制器之前以 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 新闻邮件

© . All rights reserved.