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,導致測試失敗。
  • 在您的 Prisma schema 中,email 欄位具有 @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 在測試其餘部分的響應。更具體地說,直到 mockReset 函式在 libs/__mocks__/prisma.ts 中被呼叫。

結果,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 方法被模擬後呼叫了該函式。
  4. 確保函式返回的值與您預期的一致。

透過模擬 $transaction 函式本身的響應,您不必擔心事務的順序操作(如果那是互動式事務的話)中發生了什麼。

如果您想測試一個包含重要業務邏輯的互動式事務,您需要進行驗證,該方法將無法工作,因為它完全忽略了事務的內部運作。

為了測試包含重要業務邏輯的互動式事務,您可以編寫一個看起來像以下內容的測試:

這個測試稍微複雜一些,因為它涉及到許多不同的移動部件。

以下是發生的情況:

  1. 帖子和響應物件被模擬。
  2. 模擬了 createcount 方法的響應。
  3. 模擬了 $transaction 函式的實現,以便您可以將模擬的 Prisma Client 提供給互動式事務函式,而不是實際的客戶端例項。
  4. 呼叫了 addPost 方法。
  5. 驗證響應的值,以確保互動式事務中的業務邏輯正常工作。更具體地說,它確保新帖子的 published 標誌設定為 true

監聽方法

您將探索的最後一個概念是“監聽”。Vitest,透過一個名為 TinySpy 的包,為您提供了監聽函式的能力。監聽允許您在程式碼執行過程中觀察函式,並確定諸如:它被呼叫了多少次,傳遞給它的引數是什麼,它返回的值等等。

注意:監聽函式允許您在程式碼執行時觀察函式的詳細資訊,而無需修改目標函式或其行為。

您可以使用 vi.spyOn() 監聽一個未模擬的函式,但是一個用 vi.fn() 模擬的函式預設具有所有監聽功能。由於 Prisma Client 已經被模擬,每個函式都應該能夠被監聽。

Spy functions.

以下是一個使用監聽的測試示例:

這些監聽函式在您試圖確保某些場景根據各種輸入觸發時特別有用。

為什麼選擇 Vitest?

您可能好奇為什麼本文重點關注 Vitest 作為測試框架,而不是像 Jest 這樣更成熟和流行的框架。

這個決定背後的原因與不同工具與 Node.js 的相容性有關,特別是在處理 Error 物件時。Matteo Collina(Node.js 技術指導委員會成員之一,並有其他傑出成就)在他最近的一次直播中很好地描述了這個問題。

問題的癥結在於 Jest 無法開箱即用地確定一個錯誤是否是 Error 類的例項。

這可能會在您為應用程式的不同情況編寫測試時導致各種意外問題。

它們有什麼不同?

幸運的是,大多數測試框架都非常相似,概念也相當無縫地轉換。例如,如果您習慣於使用 Jest,並正在考慮轉向 Vitest 或 node-tap(另一個測試框架),您已有的知識將很容易轉移到新技術中。

只需要進行非常小的調整:例如函式命名約定和配置。

您應該使用 Jest 嗎?

是的!Jest 是一個由非常有能力的人編寫的優秀工具。雖然 Vitest 可能是測試 Node.js 後端應用程式的“最佳工具”,但 Jest 仍然完全能夠測試前端 JavaScript 應用程式。

總結與展望

在本文中,您重點了解了“模擬”和“監聽”的概念,兩者在單元測試應用程式中都扮演著重要角色。具體來說,您探索了:

  • 什麼是模擬以及它為何有用
  • 如何配置 Prisma 和 Vitest 來設定專案
  • 如何模擬 Prisma Client
  • 如何使用模擬的 Prisma Client 例項

有了這些知識和測試領域的背景,您現在擁有了單元測試應用程式所需的工具集。在本系列的下一篇文章中,您將確切地做到這一點!

我們希望您能加入本系列的下一部分,探索測試使用 Prisma Client 的應用程式的各種方法。

不要錯過下一篇文章!

註冊 Prisma 新聞通訊

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