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 容器使用 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 镜像
  • 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 路由 /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 作为开发依赖项安装

然后在 src/tests/auth.test.ts 的顶部导入 supertest,名称为 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 发送 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 中定义)来验证其请求正文是否包含有效的 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

成功时,它应该返回用户详细信息

下一个测试与之前的测试非常相似,只是您将检查响应正文中是否存在 user 对象并验证其内容,而不是检查 200 响应状态。

添加另一个包含以下内容的测试

上面测试的内容执行以下操作

  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 新闻通讯