2022 年 12 月 22 日

Prisma 测试终极指南:模拟 Prisma Client

随着应用程序的增长,自动化测试变得越来越重要。在本文中,您将学习如何模拟 Prisma Client,以便您可以测试与数据库交互的函数,而无需访问实际的数据库。

The Ultimate Guide to Testing with Prisma: Mocking Prisma Client

目录

简介

测试在应用程序中变得越来越重要,因为它使开发人员对他们编写的代码更有信心,并能更有效地迭代他们的产品。

正如人们可能想象的那样,能够自信而高效地工作是任何开发人员工作流程的重要方面。那么...为什么不是每个开发人员都为他们的应用程序编写测试呢?这个问题的答案通常是:编写测试,尤其是在涉及数据库时,可能很棘手!

Testing meme

警告:不良建议 👆🏻

在本系列中,您将学习如何对与数据库交互的各种应用程序执行不同类型的测试。

本文将专门深入探讨模拟的主题,并逐步介绍如何模拟 Prisma Client。然后,您将了解如何使用模拟客户端。

您将使用的技术

先决条件

预期的知识

以下知识对于学习本系列文章将很有帮助

  • JavaScript 或 TypeScript 的基本知识
  • Prisma Client 及其功能的基本知识

开发环境

要跟随提供的示例进行操作,您需要具备

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

什么是模拟?

您将在本系列中看到的第一个概念是模拟。这个术语指的是为对象创建一个受控的替代品,使其行为类似于它所替代的真实对象。

模拟的目标通常是允许开发人员替换函数可能需要的任何外部依赖项,以便他们可以有效地针对该函数编写单元测试。这样,测试可以隔离到函数的行为,而无需担心不直接相关的外部模块的行为。

注意:您将在本系列的下一篇文章中更仔细地了解单元测试。

为了说明这一点,请考虑以下函数

此函数执行三件事

  1. 检查以确保提供了有效的电子邮件地址
  2. 如果提供了无效地址,则抛出错误
  3. 通过虚构的 mailer 服务发送电子邮件

要编写测试来验证此函数是否按预期运行,您可能会首先测试函数被提供无效电子邮件地址的场景,并验证是否抛出了错误。

但是,该函数依赖于两个外部代码:isValidEmailmailer。因为这些是单独的代码片段,并且在技术上与您正在测试的函数无关,所以您不希望担心这些导入是否正常运行。相反,应该假定这些是功能性的并独立测试。

您可能也不希望在调用 mailer.send() 时在测试期间发送实际的电子邮件,因为该功能与您正在测试的函数无关。

在这种情况下,通常的做法是模拟这些依赖项,而是用返回受控值的“假”对象替换真实的导入对象。这样做,您就可以在测试的目标函数中触发特定状态,而无需考虑另一个模块的行为。

这是一个相当基本的场景,说明了模拟的用途,但是本文的其余部分将更深入地探讨您可以用来模拟模块并使用这些模拟来测试特定场景的不同模式和工具。

设置 Prisma 项目

在开始编写测试之前,您需要一个项目进行实验。要进行设置,您将使用 try-prisma,这是一个允许您使用 Prisma 快速设置示例项目的工具。

在终端中运行以下命令

完成后,启动器项目应已在您当前的工作目录中名为 mocking_playground 的文件夹中设置。

您还将在终端中看到其他输出,其中包含有关后续步骤的说明。按照这些说明进入您的项目并运行您的第一个 Prisma 迁移

现在已生成 SQLite 数据库,您的模式已应用,并且已生成 Prisma Client。您已准备好开始在您的项目中工作!

设置 Vitest

为了创建测试和模拟,您需要一个测试框架。在本系列中,您将使用越来越流行的 Vitest 测试框架,该框架提供了一组工具,允许您构建和运行测试,以及创建模块的模拟。

注意:Vitest 还做了很多其他非常酷的事情!如果您好奇,请查看他们的 文档

在您的项目中运行此命令以安装 Vitest 框架及其 CLI 工具

接下来,在您项目的根目录中创建一个名为 test 的新文件夹,其中将存放您的所有测试

注意:Vitest 不要求您将测试放在 /test 文件夹中。Vitest 默认情况下会根据这些命名约定检测测试文件。

最后,在 package.json 中,添加一个名为 test 的新脚本,该脚本仅运行命令 vitest

您现在可以使用 npm run test 来运行您的测试。您也可以运行 npm t 作为简写。目前,您的测试将失败,因为没有测试文件。

/test 目录中创建一个名为 sample.test.ts 的新文件

添加以下测试,以便您可以验证 Vitest 是否已正确设置

现在有一个有效的测试,运行 npm t 应该会成功!Vitest 已设置好,可以使用了。

为什么要模拟 Prisma Client?

说明为什么模拟 Prisma Client 在单元测试中很有用的最佳方法是编写一个使用 Prisma Client 的函数,并为该函数编写一个不使用模拟客户端的测试。

在您项目的根目录中,创建一个名为 libs 的新文件夹。然后在该文件夹中创建一个名为 prisma.ts 的文件

将以下代码段添加到该新文件中

上面的代码实例化 Prisma Client 并将其导出为单例实例。这是“真实”的 Prisma Client 实例。

现在有一个可用的 Prisma Client 实例,编写一个使用它的函数。

script.ts 的内容替换为以下内容

createUser 函数执行以下操作

  1. 接受 user 参数
  2. user 传递给 prisma.user.create 函数
  3. 返回响应,应该是新的用户对象

接下来,您将为该新函数编写一个测试。此测试将确保在提供有效用户(即新用户)时,createUser 返回预期的数据。

更新 test/sample.test.ts,使其与以下代码段匹配

注意:上面的测试没有使用模拟的 Prisma Client。它使用真实的客户端实例来演示您在针对真实数据库进行测试时可能遇到的问题。

假设您的数据库尚未包含任何用户记录,则此测试在您第一次运行时应该会通过。但是,存在一些问题

  • 下次您运行此测试时,创建用户的 id 将不再是 1,从而导致测试失败。
  • email 字段在您的 Prisma 模式中具有 @unique 属性,表示该列在数据库中具有唯一索引。这将在后续运行测试时导致错误发生。
  • 此测试假定您正在针对开发数据库运行,并且需要数据库可用。每次运行此测试时,都会向数据库添加一条记录。

在诸如单元测试等专注于单个函数的情况下,最佳实践是假定您的数据库操作将正常运行,并使用客户端或驱动程序的模拟版本,从而使您可以专注于测试您正在定位的函数的特定行为。

注意:在某些情况下,您可能希望针对数据库进行测试并实际对其执行操作。集成测试和端到端测试是这些情况的良好示例。这些测试可能依赖于跨应用程序的多个函数和区域发生的多个数据库操作。

模拟 Prisma Client

由于上一节中概述的原因,创建客户端的模拟以正确地单元测试使用 Prisma Client 的函数被认为是最佳实践。此模拟将替换您的函数通常会使用的导入模块。

为了实现这一点,您将使用 Vitest 的模拟工具和一个名为 vitest-mock-extended 的外部库。

首先,在您的项目中安装 vitest-mock-extended

接下来,转到 test/sample.test.ts 文件并进行以下更改,以告知 Vitest 它应该模拟 libs/prisma.ts 模块

vi 对象中可用的 mock 函数让 Vitest 知道它应该模拟在提供的文件路径中找到的模块。mock 函数可以通过几种不同的方式来决定如何模拟目标模块,如文档中所述。

目前,Vitest 将尝试模拟在 '../libs/prisma' 中找到的模块,但是它将无法自动模拟 prisma 对象的“深层”或“嵌套”属性。例如,prisma.user.create() 将不会被正确模拟,因为它 Prisma Client 实例的深层嵌套属性。这会导致测试失败,因为该函数仍将像往常一样针对真实数据库运行。

要解决此问题,您需要让 Vitest 知道您希望如何模拟该模块,并为其提供在导入模拟模块时应返回的值,该值应包括深层嵌套属性的模拟版本。

libs 目录中创建一个名为 __mocks__ 的新文件夹

文件夹名称 __mocks__ 是测试框架中的常见约定,您可以在其中放置任何手动创建的模块模拟。__mocks__ 文件夹必须直接与您正在模拟的模块相邻,这就是为什么我们在 libs/prisma.ts 文件旁边创建了该文件夹。

在该新文件夹中,创建一个名为 prisma.ts 的文件

请注意,此文件与“真实”文件 prisma.ts 具有相同的名称。通过遵循此约定,Vitest 将知道何时通过 vi.mock 模拟模块,它应该使用该文件来查找客户端的模拟版本。

有了这个结构,您现在将创建手动模拟。

在新的 libs/__mocks__/prisma.ts 文件中,添加以下内容

上面的代码段执行以下操作

  1. 导入创建模拟客户端所需的所有工具。
  2. 让 Vitest 知道在每个单独的测试之间,模拟应重置为其原始状态。
  3. 使用 vitest-mock-extended 库的 mockDeep 函数创建并导出一个 Prisma Client 的“深层模拟”,该函数确保对象的所有属性,甚至是深层嵌套的属性,都被模拟。

注意:本质上,mockDeep 会将每个 Prisma Client 函数的值设置为 Vitest 辅助函数:vi.fn()

此时,如果您再次使用 npm t 运行测试,您应该看到您不再收到与之前相同的错误!但是仍然存在一个问题...

Failed test

查询返回 `undefined`

此错误实际上是因为模拟已正确就位而发生的。您在 script.ts 中的 prisma.user.create 调用不再访问数据库。目前,该函数基本上什么也不做,并返回 undefined

您需要通过模拟其行为来告诉 Vitest prisma.user.create 应该做什么。现在您已经有了 Prisma Client 的正确模拟版本,这需要在您的测试中进行简单的更改。

test/sample.test.ts 文件中,添加以下内容以告知 Vitest 该函数在单个测试过程中应如何表现

上面,导入了“假”客户端,因为它导出了 Prisma Client 的深层模拟。

在此对象上,您会注意到附加到每个 Prisma Client 属性和函数的一组新函数

Mock Functions

上面代码段中使用的 mockResolvedValue 将正常的 prisma.user.create 函数替换为返回提供的值的函数。在单个测试的过程中,该函数的行为将类似于您执行了以下赋值

注意:在本文的后面部分,您将深入了解模拟 Prisma Client 可用的一些有用的函数以及如何使用它们。

您现在可以通过预先模拟客户端的行为来运行使用 Prisma Client 的函数,以确保获得期望的结果。这样,您可以专注于函数的实际业务逻辑,而不是担心单个查询。

如果您现在再次运行测试,您最终应该看到您的所有测试都已通过!✅

使用模拟客户端

因此,您已经获得了模拟的 Prisma Client 实例,并且能够操纵客户端以生成您需要在函数中测试特定场景的查询结果...接下来是什么?

本文的其余部分将深入探讨您的模拟客户端和 Vitest 可用的许多函数,以及如何在不同场景中使用它们来改善您的测试体验。

注意:以下示例将不是可行的、全面的单元测试。相反,它们将是通过模拟客户端可用的工具的功能示例。本系列的下一篇文章将深入介绍单元测试。

模拟查询响应

您将使用模拟客户端最常见的操作之一是模拟查询的响应。您已经在本文前面模拟了 create 方法的响应,但是有多种方法可以做到这一点,每种方法都有自己的用例。

以这个场景为例

注意:此处使用 toStrictEqual 很重要。在比较对象时,toStrictEqual 确保对象具有相同的结构和类型。

虽然此测试成功通过,但它没有多大意义。当调用 prisma.post.findMany.mockResolvedValue 时,提供给该函数的值将用作 prisma.post.findMany 在测试剩余时间内的响应。更具体地说,直到在 libs/__mocks__/prisma.ts 中调用 mockReset 函数。

因此,unpublishedpublished 数组将包含完全相同的值,包括 published 属性中的 true 值。

为了在此场景中生成更真实的响应,您可以使用另一个函数:mockResolvedValueOnce。可以多次调用此函数以模拟函数的响应和后续调用的响应。

在您的 getPosts 函数中,您可以使用 mockResolvedValueOnce 来模拟该函数应返回的第一个和第二个响应。

注意:通过 Vitest 提供的许多函数都有一个 mockXValueOnce 方法以及 mockXValue。有关更多详细信息,请参阅文档

触发和捕获错误

您可能要测试的另一种情况是查询失败并返回或抛出错误的情况。一个很好的例子是 Prisma Client 的 findUniqueOrThrow 函数,这可能很有用。

此函数搜索唯一记录,但如果未找到记录,则抛出错误。但是,由于您的 Prisma Client 的函数是模拟的,因此 findUniqueOrThrow 函数不再以这种方式运行。您必须手动触发错误状态。下面显示了一个如何测试此行为的示例

mockImplementation 允许您提供一个函数来替换模拟函数的行为。在上面的示例中,替换函数只是抛出一个错误。

虽然乍一看这可能有点乏味,但在这种情况下需要手动定义函数的行为实际上是一个额外的好处。这使您可以精细地控制函数在不同状态(甚至是错误状态)下的输出。

与上面类似,如果您正在测试的方法旨在抛出实际错误,而不是返回一些与错误相关的消息,您也可以对此进行测试!

通过在 expect 函数的响应上使用 rejects 关键字,Vitest 知道要解析提供给 expectPromise 并查找错误响应。一旦 Promise 解析,toThrowtoThrowError 函数允许您检查有关错误的特定详细信息。

模拟事务

您可能需要模拟的 Prisma Client 的另一部分是 $transaction

事务有不同的类型:顺序操作交互式事务。您模拟这些事务的方式将很大程度上取决于您的测试目标以及您使用 $transaction 的上下文。但是,您通常可以通过两种方式模拟此函数。

对于顺序操作和交互式事务,已完成事务的结果最终都会从 $transaction 函数返回。如果您的测试只关心事务的结果,则您的测试将与上面模拟函数响应的测试非常相似。

一个示例可能如下所示

在上面的测试中,您

  1. 模拟了您打算创建的帖子的数据。
  2. 模拟了 $transaction 的响应应该是什么样子。
  3. 在 Prisma Client 方法被 mock 之后调用了该函数。
  4. 确保从你的函数返回的值与你期望的值相符。

通过 mock $transaction 函数本身的响应,你无需担心事务的顺序操作(或者交互式事务,如果适用的话)内部发生了什么。

如果你想测试一个具有重要业务逻辑的交互式事务,而你需要验证这些逻辑呢?这种方法就行不通了,因为它完全忽略了事务的内部运作方式。

要测试具有重要业务逻辑的交互式事务,你可以编写如下所示的测试:

这个测试稍微复杂一些,因为有很多不同的活动部件需要考虑。

以下是发生的情况:

  1. `post` 和 `response` 对象被 mock 了。
  2. createcount 方法的响应被 mock 了。
  3. $transaction 函数的实现被 mock 了,这样你可以将 mock 的 Prisma Client 提供给交互式事务函数,而不是实际的客户端实例。
  4. addPost 方法被调用。
  5. 响应的值被验证,以确保交互式事务内的业务逻辑正常工作。更具体地说,它确保新 post 的 published 标志被设置为 true

Spy 方法

你将探索的最后一个概念是 spying (监视)。Vitest 通过一个名为 TinySpy 的包,使你能够 spy (监视)一个函数。Spying 允许你在代码执行过程中观察一个函数,并确定诸如:它被调用了多少次、传递给它的参数是什么、它返回的值以及更多信息。

注意:Spying 一个函数允许你在代码执行时观察关于该函数的详细信息,而无需修改目标函数或其行为。

你可以使用 vi.spyOn() 监视一个未 mock 的函数,但是使用 vi.fn() mock 的函数默认情况下已经具备了所有 spying 功能。由于 Prisma Client 已经被 mock,所以每个函数都应该能够被监视。

Spy functions.

下面是一个快速示例,展示了使用 spy 的测试可能是什么样子:

当你尝试确保某些场景基于各种输入被触发时,这些 spy 函数特别有用。

为什么选择 Vitest?

你可能对为什么本文专注于 Vitest 作为测试框架,而不是像 Jest 这样更成熟和流行的框架感到好奇。

这个决定背后的原因是不同的工具与 Node.js 的兼容性,特别是处理 Error 对象时。Matteo Collina,Node.js 技术指导委员会的成员以及其他杰出成就的获得者,在他的最近一次直播中很好地描述了这个问题。

简而言之,问题在于 Jest 无法开箱即用地确定错误是否是 Error 类的实例。

当你为应用程序中不同的情况编写测试时,这可能会导致各种意想不到的问题。

它们有什么不同?

幸运的是,在很大程度上,每个测试框架都非常相似,概念可以相当无缝地转移。例如,如果你习惯于使用 Jest,并且正在考虑转向像 Vitest 或 node-tap(另一个测试框架)之类的工具,那么你已经掌握的知识将非常容易转移到新技术上。

只需要进行非常小的调整:例如函数命名约定和配置。

你还应该使用 Jest 吗?

是的!Jest 是一个由非常有能力的人编写的非常棒的工具。虽然在 Node.js 中测试后端应用程序时,Vitest 可能是“最适合这项工作的工具”,但 Jest 仍然完全能够胜任前端 JavaScript 应用程序的测试。

总结 & 接下来是什么

在本文中,你专注于 mocking (模拟)和 spying (监视)的概念,这两个概念在应用程序的单元测试中都起着重要作用。具体来说,你探索了:

  • 什么是 mocking 以及它为什么有用
  • 如何配置一个带有 Vitest 和 Prisma 的项目
  • 如何 mock Prisma Client
  • 如何使用 mock 的 Prisma Client 实例

凭借这些知识和对测试领域的理解,你现在拥有了单元测试应用程序所需的工具集。在本系列的下一篇文章中,你将实践这些!

我们希望你加入本系列的后续部分,我们将探索你可以使用各种方法来测试使用 Prisma Client 的应用程序。

不要错过下一篇文章!

订阅 Prisma 新闻通讯