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 数据库,您的 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 函数执行以下操作

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

接下来,您将为该新函数编写一个测试。此测试将确保在提供有效用户(即新用户)时,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 文件中,添加以下内容

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

  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 客户端的 findUniqueOrThrow 函数。

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

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

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

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

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

模拟事务

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

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

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

一个例子可能如下所示

在上面的测试中,您

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

通过模拟 $transaction 函数本身的响应,您不必担心事务的顺序操作(或交互式事务,如果属于这种情况)中发生了什么。

如果您想测试具有重要业务逻辑的交互式事务(您需要验证),该怎么办?此方法将不起作用,因为它完全放弃了事务的内部工作。

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

此测试涉及更多,因为要考虑很多不同的移动部件。

以下是发生的情况

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

侦听方法

您将探讨的最后一个概念是侦听。Vitest 通过一个名为 TinySpy 的包,使您能够侦听一个函数。侦听允许您在代码执行过程中观察一个函数,并确定诸如:调用了多少次、传递给它的参数、它返回的值等等。

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

您可以使用 vi.spyOn() 侦听未模拟的函数,但是具有 vi.fn() 的模拟函数默认具有所有可用的侦听功能。因为 Prisma 客户端已被模拟,所以每个函数都应该能够被侦听。

Spy functions.

下面是一个快速示例,说明使用侦听的测试可能是什么样子

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

为什么选择 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 的项目
  • 如何模拟 Prisma 客户端
  • 如何使用模拟的 Prisma 客户端实例

有了这些知识和测试世界的背景,您现在拥有了单元测试应用程序所需的工具集。在本系列的下一篇文章中,您将完全做到这一点!

我们希望您加入本系列的下一部分,我们将探索您可以使用 Prisma 客户端测试应用程序的各种方式。

不要错过下一篇文章!

注册 Prisma 新闻通讯