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 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. `post` 和 `response` 物件已被模擬 (mock)。
  2. `create` 和 `count` 方法的回應已被模擬 (mock)。
  3. `$transaction` 函數的實作已被模擬 (mock),以便您可以將模擬 (mock) 的 Prisma Client 提供給互動式事務函數,而不是實際的客戶端實例。
  4. `addPost` 方法被調用。
  5. 回應的值會被驗證,以確保互動式事務中的業務邏輯運作正常。更具體地說,它確保新貼文的 `published` 標誌被設定為 `true`。

監聽方法

最後一個您將探索的概念是監聽 (spying)。 Vitest 透過一個名為 TinySpy 的套件,讓您能夠監聽 (spy) 函數。 監聽 (Spying) 讓您可以在程式碼執行過程中觀察函數,並確定諸如:它被調用了多少次、傳遞給它的參數是什麼、它返回的值等等。

注意: 監聽 (Spying) 函數讓您可以在程式碼執行時觀察關於函數的詳細資訊,而無需修改目標函數或其行為。

您可以使用 `vi.spyOn()` 監聽 (spy) 未模擬 (mock) 的函數,但是使用 `vi.fn()` 模擬 (mock) 的函數預設已具備所有監聽 (spying) 功能。 因為 Prisma Client 已被模擬 (mock),所以每個函數都應該能夠被監聽 (spy)。

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 電子報