随着应用程序的增长,自动化测试变得越来越重要。在本文中,您将学习如何模拟 Prisma Client,以便在不访问实际数据库的情况下测试具有数据库交互的函数。
目录
- 目录
- 简介
- 先决条件
- 什么是模拟?
- 设置 Prisma 项目
- 设置 Vitest
- 为什么要模拟 Prisma Client?
- 模拟 Prisma Client
- 使用模拟客户端
- 监视方法
- 为什么选择 Vitest?
- 总结 & 后续步骤
简介
测试在应用程序中变得越来越重要,因为它使开发人员对他们编写的代码更有信心,并能更高效地迭代他们的产品。
正如人们可能想象的那样,能够自信和高效地工作是任何开发人员工作流程的重要方面。那么... 为什么不是每个开发人员都为他们的应用程序编写测试呢?这个问题的答案通常是:编写测试,尤其是在涉及到数据库时,可能很棘手!

警告:糟糕的建议 👆🏻
在本系列中,您将学习如何对与数据库交互的各种应用程序执行不同类型的测试。
本文将专门深入探讨<着重>模拟着重>主题,并逐步介绍如何模拟 Prisma Client。然后,您将了解可以使用模拟客户端执行的操作。
您将使用的技术
先决条件
假定知识
以下知识对学习本系列文章会有所帮助
- JavaScript 或 TypeScript 的基本知识
- Prisma Client 及其功能的基本知识
开发环境
要跟随提供的示例进行操作,您需要具备
什么是模拟?
您将在本系列中看到的第一个概念是<着重>模拟着重>。此术语指的是为对象创建一个受控的替代品,该替代品的作用类似于它所替代的真实对象。
模拟的目标通常是允许开发人员替换函数可能需要的任何外部依赖项,以便他们可以有效地针对该函数编写单元测试。这样,测试就可以隔离到函数的行为,而无需担心不直接相关的外部模块的行为。
注意:您将在本系列的下一篇文章中更仔细地了解单元测试。
为了说明这一点,请考虑以下函数
此函数执行三项操作
- 检查以确保提供了有效的电子邮件地址
- 如果提供了无效地址,则抛出错误
- 通过一个假想的
mailer
服务发送电子邮件
要编写一个测试来验证此函数是否按预期运行,您可能会首先测试函数被提供了无效的电子邮件地址的情况,并验证是否抛出了错误。
但是,该函数依赖于两个外部代码:isValidEmail
和 mailer
。因为这些是单独的代码,并且在技术上与您正在测试的函数无关,所以您不希望担心这些导入是否正常运行。相反,应该假定这些是功能性的,并进行独立测试。
您可能也不希望在调用 mailer.send()
时在测试期间发送实际的电子邮件,因为该功能与您正在测试的函数无关。
在这种情况下,常见的做法是<着重>模拟着重>这些依赖项,用返回受控值的“伪造”对象替换真正的导入对象。这样做,您就可以在测试的目标函数中触发特定状态,而无需考虑另一个模块的行为。
这是一个相当基本的场景,说明了模拟的用处,但是本文的其余部分将更深入地探讨可用于模拟模块并使用这些模拟来测试特定场景的不同模式和工具。
设置 Prisma 项目
在开始编写测试之前,您需要一个项目来实验。要设置一个项目,您将使用 try-prisma
,这是一个允许您快速设置带有 Prisma 的示例项目的工具。
在终端中运行以下命令
完成后,应该在您当前的工作目录中名为 mocking_playground
的文件夹中设置一个启动器项目。
您还将在终端中看到其他输出,其中包含有关后续步骤的说明。按照这些说明进入您的项目并运行您的第一个 Prisma 迁移
现在已生成 SQLite 数据库,应用了您的 schema,并且已生成 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
函数执行以下操作
- 接收一个
user
参数 - 将
user
传递给prisma.user.create
函数 - 返回响应,应该是新的用户对象
接下来,您将为该新函数编写一个测试。此测试将确保在提供有效用户(即新用户)时,createUser
返回预期的数据。
更新 test/sample.test.ts
,使其与以下代码片段匹配
注意:上面的测试没有使用模拟的 Prisma Client。它使用真实的客户端实例来演示在针对真实数据库进行测试时可能遇到的问题。
假设您的数据库尚未包含任何用户记录,则此测试在您第一次运行时应该通过。但是存在一些问题
- 下次您运行此测试时,创建的用户的
id
将不再是1
,导致测试失败。 email
字段在您的 Prisma schema 中具有@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
文件中,添加以下内容
上面的代码片段执行以下操作
- 导入创建模拟客户端所需的所有工具。
- 让 Vitest 知道在每个单独的测试之间,模拟都应重置为其原始状态。
- 使用
vitest-mock-extended
库的mockDeep
函数创建和导出一个 Prisma Client 的“深层模拟”,该函数确保对象的所有属性(甚至是深层嵌套的属性)都被模拟。
注意:本质上,
mockDeep
会将每个 Prisma Client 函数的值设置为 Vitest 帮助程序函数:vi.fn()
。
此时,如果您再次使用 npm t
运行测试,您应该看到您不再收到与之前相同的错误!但是仍然存在一个问题...

查询返回 `undefined`
此错误实际上是因为模拟已正确就位而发生的。您的 script.ts
中的 prisma.user.create
调用不再访问数据库。目前,该函数基本上什么也不做,并返回 undefined
。
您需要通过<着重>模拟着重>其行为来告诉 Vitest prisma.user.create
应该做什么。现在您有了一个正确的 Prisma Client 模拟版本,这只需要对您的测试进行简单的更改。
在 test/sample.test.ts
文件中,添加以下内容以告知 Vitest 该函数在该单个测试过程中应如何运行
上面,导入了“伪造”客户端,因为它导出了 Prisma Client 的深层模拟。
在此对象上,您会注意到一组新的函数附加到每个 Prisma Client 属性和函数

上面代码片段中使用的 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
函数为止。
因此,unpublished
和 published
数组将包含完全相同的值,包括 published
属性中的 true
值。
为了在这种情况下生成更真实的响应,您可以使用另一个函数:mockResolvedValueOnce
。可以多次调用此函数以模拟函数的响应以及后续调用的响应。
在您的 getPosts
函数中,您可以使用 mockResolvedValueOnce
来模拟该函数应返回的第一个和第二个响应。
注意:通过 Vitest 可用的许多函数都有一个
mockXValueOnce
方法以及mockXValue
。有关更多详细信息,请参阅 文档。
触发和捕获错误
您可能要测试的另一个场景是查询失败并返回或抛出错误的情况。一个很好的例子是 Prisma Client 的 findUniqueOrThrow
函数。
此函数搜索唯一记录,但如果未找到记录,则抛出错误。但是,由于您的 Prisma Client 的函数是模拟的,因此 findUniqueOrThrow
函数不再以这种方式运行。您必须手动触发错误状态。下面显示了一个如何测试此行为的示例
mockImplementation
允许您提供一个函数来替换模拟函数的行为。在上面的示例中,替换函数只是抛出一个错误。
虽然乍一看这似乎有点乏味,但在这种情况下需要手动定义函数的行为实际上是一个额外的好处。这使您可以精细地控制函数在不同状态(甚至是错误状态)下的输出。
与上述类似,如果您正在测试的方法旨在抛出实际错误,而不是返回与错误相关的某些消息,您也可以对其进行测试!
通过在 expect
函数的响应上使用 rejects
关键字,Vitest 知道要解析提供给 expect
的 Promise
并查找错误响应。一旦 Promise
解析,toThrow
和 toThrowError
函数允许您检查有关错误的特定详细信息。
模拟事务
您可能需要模拟的 Prisma Client 的另一部分是 $transaction
。
事务有不同的类型:顺序操作和 交互式事务。您模拟这些事务的方式将很大程度上取决于您的测试目标以及您使用 $transaction
的上下文。但是,通常有两种模拟此函数的方式。
对于顺序操作和交互式事务,已完成事务的结果最终从 $transaction
函数返回。如果您的测试只关心事务的结果,则您的测试将与上面模拟函数响应的测试非常相似。
一个例子可能如下所示
在上面的测试中,您
- 模拟了您打算创建的帖子的数据。
- 模拟了
$transaction
的响应应是什么样子。 - 在 Prisma Client 方法被模拟后调用了该函数。
- 确保函数返回的值与您期望的值匹配。
通过模拟 $transaction
函数本身的响应,您不必担心事务的顺序操作(或交互式事务,如果是这种情况)中发生了什么。
如果您想测试一个具有重要业务逻辑需要验证的交互式事务怎么办?此方法将不起作用,因为它完全放弃了事务的内部运作。
要测试具有重要业务逻辑的交互式事务,您可以编写一个如下所示的测试
此测试稍微复杂一些,因为有很多不同的活动部分需要考虑。
以下是发生的事情
- 模拟了帖子和响应对象。
- 模拟了
create
和count
方法的响应。 - 模拟了
$transaction
函数的实现,以便您可以将模拟的 Prisma Client 提供给交互式事务函数,而不是实际的客户端实例。 - 调用了
addPost
方法。 - 验证了响应的值,以确保交互式事务中的业务逻辑正常运行。更具体地说,它确保新帖子的
published
标志设置为true
。
监视方法
您将探索的最后一个概念是<着重>监视着重>。Vitest 通过一个名为 TinySpy 的软件包,使您能够<着重>监视着重>一个函数。监视允许您在代码执行过程中观察一个函数,并确定诸如:它被调用了多少次,传递给它的参数是什么,它返回的值等等。
注意:监视一个函数允许您在代码执行时观察有关该函数的详细信息,而无需修改目标函数或其行为。
您可以使用 vi.spyOn()
监视一个未模拟的函数,但是具有 vi.fn()
的模拟函数默认情况下具有所有可用的监视功能。由于 Prisma Client 已被模拟,因此每个函数都应该能够被监视。

以下是一个快速示例,说明使用<着重>监视器着重>的测试可能是什么样子
当您尝试确保基于各种输入触发某些场景时,这些<着重>监视器着重>函数特别有用。
为什么选择 Vitest?
您可能对为什么本文重点介绍 Vitest 作为测试框架,而不是更成熟和流行的框架(如 Jest)感到好奇。
此决定的背后原因与不同工具与 Node.js 的兼容性有关,尤其是在处理 Error
对象时。Matteo Collina 是 Node.js 技术指导委员会的成员,以及其他令人敬畏的成就,他在最近的一次直播中很好地描述了这个问题。
简而言之,问题是 Jest 无法开箱即用地确定错误是否是 Error
类的实例。
当您为应用程序中的不同情况编写测试时,这可能会导致各种意外问题。
它们有什么不同?
幸运的是,在很大程度上,每个测试框架都非常相似,并且概念可以相当无缝地转移。例如,如果您习惯于使用 Jest,并且正在考虑迁移到像 Vitest 或 node-tap
(另一个测试框架)之类的东西,那么您已经拥有的知识将非常容易转移到新技术。
只需要进行非常小的调整:例如函数命名约定和配置。
您应该永远使用 Jest 吗?
是的!Jest 是一个由非常有能力的人编写的出色工具。虽然在 Node.js 中测试后端应用程序时,Vitest 可能是“最适合这项工作的工具”,但 Jest 仍然完全能够胜任测试前端 JavaScript 应用程序。
总结 & 后续步骤
在本文中,您专注于<着重>模拟着重>和<着重>监视着重>的概念,这两个概念都在应用程序的单元测试中起着重要作用。具体来说,您探索了
- 什么是模拟以及为什么它有用
- 如何配置 Vitest 和 Prisma 项目
- 如何 Mock Prisma Client
- 如何使用 Mock 后的 Prisma Client 实例
有了这些关于测试的知识和背景,你现在拥有了单元测试应用程序所需的工具集。在本系列的下一篇文章中,你将 точно 这样做!
我们希望你能加入本系列的后续部分,一起探索各种测试使用 Prisma Client 的应用程序的方法。
不要错过下一篇文章!
订阅 Prisma Newsletter