2023 年 1 月 31 日

使用 Prisma 进行测试的终极指南:单元测试

单元测试涉及测试单独的、隔离的代码单元,以确保它们按预期工作。在本文中,您将学习如何识别代码库中应进行单元测试的区域、如何编写这些测试以及如何处理针对使用 Prisma Client 的函数的测试。

The Ultimate Guide to Testing with Prisma: Unit Testing

目录

简介

单元测试是确保应用程序中各个代码单元(例如函数)按预期运行的主要方法之一。

对于刚接触测试的人来说,理解什么是单元测试可能非常困难。他们不仅要理解应用程序的工作原理、如何编写测试以及如何准备测试环境,而且还必须理解他们应该测试什么!

因此,开发人员通常采用这种测试方法

注意:感谢 @RoxCodes 的坦诚 😉

在本系列中,您将使用一个功能齐全的应用程序。其代码库中唯一缺少的是一套测试,以验证其按预期工作。

在本系列文章中,您将考虑代码的各个方面,并逐步了解应该测试什么、为什么需要测试以及如何编写这些测试。这将包括单元测试集成测试端到端测试,以及设置运行这些测试的持续集成 (CI) 和持续开发 (CD) 工作流。

特别是在本文中,您将深入研究代码的特定区域并对其编写单元测试,以确保这些区域的各个构建块正常工作。

什么是单元测试?

单元测试是一种测试类型,涉及对小的、隔离的代码片段编写测试。单元测试针对小的代码单元,以确保它们在各种情况下按预期工作。

通常,单元测试将针对单个 function,因为函数通常是 JavaScript 应用程序中最小的单一代码单元。

以下函数为例

这个函数虽然简单,但非常适合进行单元测试。它包含一个单一的功能集,封装在一个函数中。为了确保这个函数正常工作,您可能会给它提供字符串 'abcde',并确保返回字符串 'edcba'

相关的测试套件或测试集可能如下所示

正如您可能已经注意到的,单元测试的目标仅仅是确保应用程序最小的构建块正常工作。通过这样做,您可以建立信心,当您开始组合这些构建块时,最终行为是可预测的。

Test graphic

其重要性如上所示。运行单元测试时,如果所有测试都通过,则可以确定每个构建块都正常工作,因此您的应用程序按预期工作。但是,如果有一个测试失败,则可以假设您的应用程序未按预期工作,并且您将根据失败的测试确切地知道问题所在。

什么不是单元测试?

在单元测试中,目标是确保您的自定义代码按预期工作。上一句中需要注意的重要一点是短语“自定义代码”

作为 JavaScript 开发人员,您可以通过 npm 访问丰富的社区构建模块和包生态系统。使用外部库可以节省大量时间,否则您可能会重复造轮子。

虽然使用外部模块没有错,但在考虑测试使用这些模块的函数时,需要考虑一些因素。最重要的是,务必牢记这一点

如果您不信任某个外部包,并且觉得应该对其编写测试,那么您可能不应该使用该特定包。

以下面的函数为例

此函数接收正方形的一条边长,并返回一个包含更精确正方形的对象,其中包括正方形的唯一颜色。

在为上述函数编写单元测试时,您可能希望验证以下内容

  • 当提供的数字小于 1 时,函数返回 null
  • 函数正确计算面积
  • 函数返回具有正确值的正确形状的对象
  • randomColor 函数被调用了一次

请注意,没有提及确保每个正方形都获得唯一颜色的测试。这是因为 randomColor 被假定为正常工作,因为它是一个外部模块。

注意:无论 randomColor 是通过 npm 包提供还是在另一个文件中自定义构建的函数,在此上下文中都应假定它能正常工作。如果 randomColor 是您在另一个文件中编写的函数,则应在其自己的独立上下文中进行测试。请记住“构建块”!

这个概念很重要,因为它也适用于 Prisma Client。当在您的应用程序中使用 Prisma 时,Prisma Client 是一个外部模块。因此,任何测试都应该假定您的客户端提供的函数按预期工作。

您将使用的技术

先决条件

假定知识

进入本系列时,以下内容会有所帮助

  • JavaScript 或 TypeScript 基础知识
  • Prisma Client 及其基本功能的知识
  • 有一些 Express 经验会很好

开发环境

要跟随提供的示例,您需要具备

  • 已安装Node.js
  • 您选择的代码编辑器(我们推荐 VSCode
  • 已安装Git

本系列将大量使用此 GitHub 存储库。请务必克隆存储库并查看 main 分支。

克隆存储库

在您的终端中,进入您存储项目的目录。在该目录中运行以下命令

上述命令会将项目克隆到一个名为 express_sample_app 的文件夹中。该存储库的默认分支是 main,因此此时您应该已准备就绪!

克隆存储库后,需要执行几个步骤来设置项目。

首先,导航到项目并安装 node_modules

接下来,在项目根目录创建一个 .env 文件

此文件应包含一个名为 API_SECRET 的变量,其值您可以设置为任何您想要的 string,以及一个名为 DATABASE_URL 的变量,目前可以留空

.env 中,API_SECRET 变量提供了一个密钥,用于身份验证服务加密您的密码。在实际应用程序中,此值应替换为包含数字和字母字符的长随机字符串。

DATABASE_URL,顾名思义,包含您的数据库 URL。您目前没有也不需要真实数据库。

最后,您需要根据 Prisma schema 生成 Prisma Client

探索 API

既然您对单元测试是什么和不是什么有了大致的了解,那么接下来看看本系列中要测试的应用程序。

您从 Github 克隆的项目包含一个功能齐全的 Express API。此 API 允许用户登录、存储和组织他们喜欢的引用。

应用程序文件按功能组织到 src 目录中的文件夹中。

src 中有三个主要文件夹

  • /auth:包含与 API 身份验证直接相关的所有文件
  • /quotes:包含与 API 引用功能直接相关的所有文件
  • /lib:包含任何通用辅助文件

API 本身提供以下端点

端点描述POST /auth/signup使用用户名和密码创建一个新用户。POST /auth/signin使用用户名和密码登录用户。GET /quotes返回与已登录用户相关的所有引用。POST /quotes存储与已登录用户相关的新引用。DELETE /quotes/:id按 ID 删除属于已登录用户的引用。

您可以花一些时间探索此项目中的文件,并了解 API 的工作原理。

对单元测试以及应用程序的工作原理有了大致了解后,您现在就可以开始编写测试以验证应用程序是否按预期工作。

注意:在实际环境中,这些测试将有助于确保随着应用程序的演变和更改,现有功能保持不变。测试可能在开发应用程序时编写,而不是在应用程序完成后编写。

设置 Vitest

为了开始测试,您需要设置一个测试框架。在本系列中,您将使用 Vitest

首先使用以下命令安装 vitestvitest-mock-extended

注意:有关上述两个包的信息,请务必阅读本系列的第一篇文章

接下来,您需要配置 Vitest,以便它知道您的单元测试在哪里以及如何解析您可能需要导入到这些测试中的任何模块。

在项目根目录中创建一个名为 vitest.config.unit.ts 的新文件

此文件将使用 Vitest 提供的 defineConfig 函数定义并导出单元测试的配置

上面您为 Vitest 配置了两个选项

  • test.include 选项告诉 Vitest 在 src 目录中任何符合 *.test.ts 命名约定的文件中查找测试。
  • resolve.alias 配置设置文件路径别名。这允许您缩短文件导入路径,例如:src/auth/auth.service 变为 auth/auth.service

最后,为了更轻松地运行测试,您将在 package.json 中配置脚本以运行 Vitest CLI 命令。

将以下内容添加到 package.jsonscripts 部分

上面添加了两个新脚本

  • test:unit:使用您在上面创建的配置文件运行 vitest CLI 命令。
  • test:unit:ui:使用您在上面创建的配置文件以UI 模式运行 vitest CLI 命令。这会在您的浏览器中打开一个带有搜索、过滤和查看测试结果工具的 GUI。

要运行这些命令,您可以在项目根目录的终端中执行以下命令

注意:如果您现在运行其中任何一个命令,您会发现命令失败。那是因为没有测试可运行!

此时,Vitest 已配置完毕,您已准备好开始考虑编写单元测试。

不需要测试的文件

在直接开始编写测试之前,您将首先查看不需要测试的文件,并思考为什么。

以下是不需要测试的文件列表

  • src/index.ts
  • src/auth/auth.router.ts
  • src/auth/auth.schemas.ts
  • src/quotes/quotes.router.ts
  • src/quotes/quotes.schemas.ts
  • src/quotes/quotes.service.ts
  • src/lib/prisma.ts
  • src/lib/createServer.ts

这些文件没有任何需要单元测试的自定义行为。

在接下来的两节中,您将查看这些文件中导致它们不需要测试的两个主要场景。

文件没有自定义行为

查看应用程序中的以下示例

src/quotes/quotes.router.ts 中,实际发生的唯一事情是 Express 框架提供的函数的调用。有一些自定义函数 (validateQuoteController.*) 正在运行,但它们在单独的文件中定义,并将在它们自己的上下文中进行测试。

第二个文件 src/auth/auth.schemas.ts 非常相似。虽然此文件对应用程序很重要,但这里确实没有什么可测试的。代码只是导出使用外部模块 zod 定义的模式。

函数只调用外部模块

另一个需要指出的场景是 src/quotes/quotes.service.ts 中的场景

此服务导出两个函数。这两个函数都封装了 Prisma Client 函数调用并返回结果。

如本文前面所述,无需测试外部代码。因此,此文件可以跳过。

如果您查看上述列表中不需要测试的其余文件,您会发现每个文件都因此处概述的原因之一而不需要测试。

您将测试什么

项目中剩余的 .ts 文件都包含需要进行单元测试的功能。需要测试的完整文件列表如下

  • src/auth/auth.controller.ts
  • src/auth/auth.service.ts
  • src/lib/middlewares.ts
  • src/lib/utility-classes.ts
  • src/quotes/quotes.controller.ts
  • src/quotes/tags.service.ts

这些文件中每个函数都应该有自己的一套测试,以验证它是否行为正确。

正如您可能想象的,这会导致很多测试!以数字表示,Express API 包含十三种不同的函数需要测试,并且每个函数可能都有一套包含两个以上测试的套件。这意味着至少要编写二十六个测试!

为了使本文长度适中,您将为单个文件 src/quotes/tags.service.ts 编写测试,因为此文件的测试涵盖了本文希望涵盖的所有重要单元测试概念。

注意:如果您对这个 API 的完整测试集是什么样子感到好奇,GitHub 存储库的 unit-tests 分支包含了每个函数的完整测试集。

测试标签服务

标签服务导出两个函数,upsertTagsdeleteOrphanedTags

首先,在与 tags.service.ts 相同的目录中创建一个新文件,名为 tags.service.test.ts

注意:组织测试的方法有很多种。在本系列中,测试将与测试目标文件一起编写,这种方法也称为将测试并置。

如果您使用的是 VSCode 且版本为 v1.64 或更高版本,您可以使用一项很酷的功能,该功能在并置测试及其目标时清理项目的文件树。

在 VSCode 中,转到屏幕顶部的选项栏中的 代码 > 首选项 > 设置

在设置页面中,输入 file nesting 搜索文件嵌套设置。启用以下设置

File nesting option in VSCode

接下来,在该设置中向下滚动一点,您将看到一个 资源管理器 > 文件嵌套:模式 部分。

如果不存在名为 *.ts 的项,则创建一个。然后将 *.ts 项的值更新为 ${capture}.*.ts

File nesting setting in VSCode

这允许 VSCode 将任何文件嵌套在名为 ${capture}.ts 的主文件下。为了更好地说明,请参阅以下示例

Nested files

上面您可以看到一个名为 quotes.controller.ts 的文件。嵌套在该文件下的是 quotes.controller.test.ts。虽然并非严格必要,但此设置可能有助于在并置单元测试时稍微清理您的文件树。

导入所需的模块

在新文件 tags.service.test.ts 的顶部,您需要导入一些允许您编写测试的内容

以下是每个导入的用途

  • TagsService:这是您正在编写测试的服务。您需要导入它才能调用其函数。
  • prismaMock:这是 lib/__mocks__/prisma 提供的 Prisma Client 的模拟版本。
  • randomColorupsertTags 函数中用于生成随机颜色的库。
  • describevitest 提供的一个函数,允许您描述一组测试。

需要注意的是 prismaMock 导入。这是模拟的 Prisma Client 实例,它允许您执行 prisma 查询而无需实际访问数据库。因为它被模拟了,您还可以操纵查询响应并监视其方法。

注意:如果您不确定 prismaMock 导入是什么以及它是如何工作的,请务必阅读本系列中涵盖此模块作用的上一篇文章

描述测试套件

您现在可以使用 Vitest 提供的 describe 函数来描述这组特定的测试

这将在输出测试结果时将此文件中的测试分组到一个部分中,从而更容易查看哪些套件通过和失败。

模拟目标文件使用的任何模块

在编写实际测试套件之前,要做的最后一件事是模拟 tags.service.ts 文件中使用的外部模块。这将使您能够控制这些模块的输出,并确保您的测试不受外部代码的污染。

在此服务中,有两个模块需要模拟:PrismaClientrandomColor

通过添加以下内容来模拟这些模块

上面,使用 Vitest 的自动模拟检测算法模拟了 lib/prisma 模块,该算法在“真实”Prisma 模块的同一目录中查找名为 __mocks__ 的文件夹和 __mocks__/prisma.ts 文件。此文件的导出用作模拟模块,以取代真实模块的导出。

randomColor 模拟有点不同,因为该模块仅导出一个默认值,它是一个函数。 vi.mock 的第二个参数是一个函数,它返回模块在导入时应该返回的对象。上面的代码片段向此对象添加了一个 default 键,并将其值设置为一个可侦测的函数,其静态返回值为 '#ffffff'

在测试套件的上下文中,beforeEachvi.restoreAllMocks 用于确保在每个单独的测试之间,模拟都被恢复到其原始状态。这很重要,因为在某些测试中,您将为该特定测试修改模拟的行为。

注意:如果您不确定这些模拟的工作原理,请务必参考本系列中涵盖模拟的上一篇文章

每当在 TagsService 中导入这些模块时,现在将导入模拟版本。

测试 upsertTags 函数

upsertTags 函数接收一个标签名称数组,并为每个名称创建一个新标签。但是,如果数据库中存在同名标签,它将不会创建该标签。该函数的返回值是与提供给函数的所有标签名称(包括新的和现有的)关联的标签 ID 数组。

在测试套件中 beforeEach 调用下方,添加另一个 describe 来描述与 upsertTags 函数相关的测试套件。同样,这样做是为了对测试输出进行分组,从而更容易查看哪些测试与此特定函数相关联。

现在是时候决定您编写的测试应该涵盖什么了。查看 upsertTags 函数,考虑它有哪些特定行为。每个期望的行为都应该被测试。

下面添加了注释,显示了此函数中应测试的每个行为。注释已编号,表示测试的编写顺序

准备好要测试的场景列表后,您现在可以开始为每个场景编写测试。

验证函数返回标签 ID 列表

第一个测试将确保函数的返回值是标签 ID 数组。在此函数的 describe 块中,添加新测试

上面的测试执行以下操作

  1. 模拟 Prisma Client 的 $transaction 函数的响应
  2. 调用 upsertTags 函数
  3. 确保函数的响应等于 $transaction 的预期模拟响应

此测试很重要,因为它专门测试函数的预期结果。如果此函数将来发生更改,此测试可确保函数的结果保持预期。

注意:如果您不确定 Vitest 提供的特定方法的作用,请参阅 Vitest 的文档

如果您现在运行 npm run test:unit,您应该会看到测试成功通过。

验证函数只创建不存在的标签

上面计划的下一个测试将验证函数不会在数据库中创建重复的标签。

该函数提供了一个表示标签名称的字符串列表。该函数首先检查具有这些名称的现有标签,并根据结果过滤,只创建新标签。

测试应

  • 模拟 prisma.tag.findMany 的第一次调用,使其返回一个标签。这表示根据提供给函数的名称找到了一个现有标签。
  • 使用三个标签名称调用 upsertTags。其中一个名称应为 tag1,即模拟现有标签的名称。
  • 确保 prisma.tag.createMany 仅提供了与 tag1 不匹配的两个标签。

upsertTags 函数的 describe 块中,在前面的测试下方添加以下测试

再次运行 npm run test:unit 现在应该显示您的两个通过的测试。

验证函数为新标签赋予随机颜色

在接下来的测试中,您需要验证每当创建新标签时,都会为其提供新的随机颜色。

为此,请编写一个基本测试,插入三个新标签。在调用 upsertTags 函数后,您可以确保 randomColor 函数被调用了三次。

以下代码片段显示了此测试应如何进行。在 upsertTags 函数的 describe 块中,在您之前编写的测试下方添加新测试

npm run test:unit 命令应导致三个成功的测试。

您可能想知道上面的测试是如何检查 randomColor 被调用了多少次。

请记住,在此文件的上下文中,randomColor 模块被模拟,并且其默认导出被配置为 vi.fn,它提供一个返回静态字符串值的函数。

因为使用了 vi.fn,所以模拟函数现在在 Vitest 中注册为您可以监视的函数。

因此,您可以使用特殊属性,例如在当前测试期间函数被调用的次数。

验证函数在其返回数组中包含新创建的标签 ID

在此测试中,您需要验证函数是否返回与提供给函数的每个标签名称关联的标签 ID。这意味着它应该返回现有标签 ID 和任何新创建标签的 ID。

此测试应

  1. 使 tag.findMany 的第一次调用返回一个标签,以模拟找到一个现有标签
  2. 模拟 tag.createMany 的响应
  3. 使 tag.findMany 的第二次调用返回两个标签,表示它找到了两个新创建的标签
  4. 使用三个标签调用 upsertTags 函数
  5. 确保返回所有三个 ID

添加以下测试来完成此任务

通过运行 npm run test:unit 来验证上述测试是否有效。

验证当未提供任何标签名称时,函数返回一个空数组

正如您所料,如果未向此函数提供任何标签名称,它将无法返回任何标签 ID。

在此测试中,通过添加以下内容来验证此行为是否正常工作

至此,为该函数确定的所有场景都已测试完毕!

如果您使用添加到 package.json 的任何一个脚本运行测试,您应该会看到所有测试都成功运行并通过!

注意:如果您尚未运行此命令,系统可能会提示您安装 @vitest/ui 包并重新运行该命令。

Successful suite of tests

测试 deleteOrphanedTags 函数

此函数与上一个函数的情况非常不同。

正如您可能已经确定的,此函数只是封装了 Prisma Client 函数的调用。正因为如此……您猜对了!此函数实际上不需要测试!

总结和下一步

在本文中,您

  • 了解了什么是单元测试以及它对您的应用程序的重要性
  • 看到了几个单元测试并非严格必要的情况示例
  • 设置 Vitest
  • 学习了一些在编写测试时简化工作的小技巧
  • 尝试为 API 中的服务编写单元测试

虽然本文只涵盖了 quotes API 中的一个文件,但用于测试标签服务的概念和方法也适用于应用程序的其余部分。我鼓励您为 API 的其余部分编写测试以进行练习!

在本系列的下一部分中,您将深入研究集成测试并为这个相同的应用程序编写集成测试。

不要错过下一篇文章!

订阅 Prisma 新闻通讯

© . This site is unofficial and not affiliated with Prisma Data, Inc.