2023年1月31日

Prisma测试终极指南:单元测试

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

The Ultimate Guide to Testing with Prisma: Unit Testing

目录

引言

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

对于测试新手来说,理解什么是单元测试可能非常困难。他们不仅要理解应用程序如何工作、如何编写测试以及如何准备测试环境,还要理解他们应该测试什么!

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

注意:感谢@RoxCodes的坦诚 😉

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

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

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

什么是单元测试?

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

通常,单元测试将针对单个函数,因为函数通常是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定义的schema。

函数只调用外部模块

另一个需要指出的场景是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的模拟版本。
  • randomColor:在upsertTags函数中用于生成随机颜色的库。
  • describe:一个由vitest提供的函数,允许您描述一个测试套件。

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

注意:如果您不确定prismaMock导入是什么以及它是如何工作的,请务必参考本系列中介绍模拟的前一篇文章

描述测试套件

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

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

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

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

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

通过添加以下代码来模拟这些模块

上面,lib/prisma模块使用Vitest的自动模拟检测算法进行模拟,该算法在“真实”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新闻通讯

© . All rights reserved.