2023 年 1 月 31 日

Prisma 測試終極指南:單元測試

單元測試涉及測試個別、隔離的程式碼單元,以確保它們如預期般運作。在本文中,您將學習如何識別程式碼庫中應進行單元測試的區域、如何編寫這些測試,以及如何處理針對使用 Prisma Client 的函數的測試。

The Ultimate Guide to Testing with Prisma: Unit Testing

目錄

簡介

單元測試是確保應用程式中個別程式碼單元(例如函數)如您預期運作的主要方法之一。

對於剛接觸測試的人來說,要理解什麼是單元測試可能非常困難。他們不僅必須了解其應用程式的運作方式、如何編寫測試以及如何準備測試環境,而且還必須了解他們應該測試什麼!

因此,開發人員通常會採用這種測試方法

注意:感謝 @RoxCodes 的誠實 😉

在本系列中,您將使用一個功能完整的應用程式。其程式碼庫中唯一缺少的是一套驗證其運作是否正常的測試。

在本系列課程中,您將考慮程式碼的各個領域,並逐步了解應該測試什麼、為什麼需要測試以及如何編寫這些測試。這將包括單元測試整合測試端對端測試,以及設定執行這些測試的持續整合 (CI) 和持續開發 (CD) 工作流程。

在本文中,您將特別深入研究程式碼的特定區域,並針對它們編寫單元測試,以確保這些區域的個別建構區塊運作正常。

什麼是單元測試?

單元測試是一種針對小型、隔離的程式碼片段編寫測試的測試類型。單元測試的目標是小型程式碼單元,以確保它們在各種情況下如預期般運作。

通常,單元測試會以個別 函數 為目標,因為函數通常是 JavaScript 應用程式中最小的單一程式碼單元。

以下列函數為例

雖然這個函數很簡單,但它是單元測試的良好候選者。它包含一組單一的功能,並封裝在一個函數中。為了確保此函數運作正常,您可以向其提供字串 'abcde',並確保傳回字串 'edcba'

相關的套件測試或測試集可能如下所示

正如您可能已在上面注意到的,單元測試的目標只是確保應用程式中最小的建構區塊運作正常。透過這樣做,您可以建立信心,隨著您開始組合這些建構區塊,產生的行為是可預測的。

Test graphic

之所以如此重要的原因是如上所示。當您執行單元測試時,如果所有測試都通過,您可以確定每個建構區塊都運作正常,因此,您的應用程式也如預期般運作。但是,如果即使一個測試失敗,您也可以假設您的應用程式運作不正常,並且您可以根據失敗的測試準確地知道問題所在。

什麼不是單元測試?

在單元測試中,目標是確保您的自訂程式碼如預期般運作。從上一個句子中需要注意的重點是「自訂程式碼」這個詞組。

作為 JavaScript 開發人員,您可以透過 npm 存取由社群建立的豐富模組和套件生態系統。使用外部程式庫可讓您節省大量時間,否則您可能會重新發明輪子。

雖然使用外部模組沒有什麼問題,但在考慮測試使用這些模組的函數時,仍有一些注意事項需要考慮。最重要的是,請記住這一點

如果您不信任外部套件,並且認為應該針對它編寫測試,那麼您可能不應該使用該特定套件。

以下列函數為例

此函數接受正方形一邊的長度,並傳回一個包含更精確定義的正方形的物件,包括正方形的獨特顏色。

在為上述函數編寫單元測試時,您可能需要驗證以下內容

  • 當提供的數字小於一時,函數傳回 null
  • 函數正確計算面積
  • 函數傳回具有正確值的正確形狀的物件
  • randomColor 函數被呼叫一次

請注意,沒有提到測試以確保每個正方形實際上都獲得獨特的顏色。這是因為 randomColor 假定可以正常運作,因為它是一個外部模組。

注意:無論 randomColor 是透過 npm 套件提供,還是甚至是另一個檔案中的自訂建構函數,都應假定它在此環境中運作正常。如果 randomColor 是您在另一個檔案中編寫的函數,則應在其自身的隔離環境中進行測試。想想「建構區塊」!

這個概念很重要,因為它也適用於 Prisma Client。在您的應用程式中使用 Prisma 時,Prisma Client 是一個外部模組。因此,任何測試都應假定您的用戶端提供的函數如預期般運作。

您將使用的技術

先決條件

假設知識

以下知識對於開始本系列課程會很有幫助

  • JavaScript 或 TypeScript 的基本知識
  • Prisma Client 及其功能的基礎知識
  • 有一些 Express 的經驗會很好

開發環境

若要跟著提供的範例進行操作,您需要具備

  • 已安裝 Node.js
  • 您選擇的程式碼編輯器(我們建議使用 VSCode
  • 已安裝 Git

本系列將大量使用這個 GitHub 儲存庫。請務必複製儲存庫並簽出 main 分支。

複製儲存庫

在您的終端機中,前往您儲存專案的目錄。在該目錄中執行下列命令

上面的命令會將專案複製到名為 express_sample_app 的資料夾中。該儲存庫的預設分支是 main,因此此時您應該已準備就緒!

複製儲存庫後,需要執行幾個步驟來設定專案。

首先,導覽至專案並安裝 node_modules

接下來,在專案的根目錄建立 .env 檔案

此檔案應包含一個名為 API_SECRET 的變數,您可以將其值設定為您想要的任何 字串,以及一個名為 DATABASE_URL 的變數,目前可以將其留空

.env 中,API_SECRET 變數提供驗證服務用於加密密碼的秘密金鑰。在真實世界的應用程式中,此值應替換為包含數字和字母字元的長隨機字串。

DATABASE_URL,顧名思義,包含資料庫的 URL。您目前沒有或不需要真實的資料庫。

最後,您需要根據您的 Prisma 結構描述產生 Prisma Client

探索 API

現在您對單元測試是什麼以及不是什麼有了大致的了解,請看看您將在本系列中測試的應用程式。

您從 Github 複製的專案包含一個功能完整的 Express API。此 API 允許使用者登入、儲存和組織他們最喜歡的引言。

應用程式的檔案依功能組織到 src 目錄中的資料夾中。

src 中,有三個主要資料夾

  • /auth:包含所有與 API 驗證直接相關的檔案
  • /quotes:包含所有與 API 引言功能直接相關的檔案
  • /lib:包含任何一般協助程式檔案

API 本身提供下列端點

端點描述
POST /auth/signup使用使用者名稱和密碼建立新使用者。
POST /auth/signin使用使用者名稱和密碼登入使用者。
GET /quotes傳回與登入使用者相關的所有引言。
POST /quotes儲存與登入使用者相關的新引言。
DELETE /quotes/:id依 ID 刪除屬於登入使用者的引言。

隨時花一些時間探索此專案中的檔案,並了解 API 的運作方式。

對單元測試是什麼以及應用程式如何運作有了大致的了解後,您現在可以開始編寫測試以驗證應用程式是否執行預期操作的過程。

注意:在真實世界的設定中,這些測試將有助於確保隨著應用程式的發展和變更,現有的功能保持不變。測試可能會在您開發應用程式時編寫,而不是在應用程式完成後編寫。

設定 Vitest

為了開始測試,您需要設定測試框架。在本系列中,您將使用 Vitest

首先,使用下列命令安裝 vitestvitest-mock-extended

注意:如需有關上面安裝的兩個套件的資訊,請務必閱讀本系列中的第一篇文章

接下來,您需要設定 Vitest,以便它知道您的單元測試在哪裡,以及如何解析您可能需要匯入到這些測試中的任何模組。

在專案的根目錄中建立一個名為 vitest.config.unit.ts 的新檔案

此檔案將使用 Vitest 提供的 defineConfig 函數,定義和匯出單元測試的設定

在上面,您為 Vitest 設定了兩個選項

  • test.include 選項告訴 Vitest 在 src 目錄中尋找符合命名慣例 *.test.ts 的任何檔案中的測試。
  • resolve.alias 設定設定檔案路徑別名。這可讓您縮短檔案匯入路徑,例如:src/auth/auth.service 變成 auth/auth.service

最後,為了更輕鬆地執行測試,您將在 package.json 中設定指令碼,以執行 Vitest CLI 命令。

將下列內容新增至 package.jsonscripts 區段

上面新增了兩個新的指令碼

  • test:unit:這會使用您在上面建立的設定檔執行 vitest CLI 命令。
  • test:unit:ui:這會使用您在上面建立的設定檔在ui 模式下執行 vitest CLI 命令。這會在您的瀏覽器中開啟一個 GUI,其中包含搜尋、篩選和檢視測試結果的工具。

若要執行這些命令,您可以在專案根目錄的終端機中執行下列命令

注意:如果您現在執行其中任何一個命令,您會發現命令失敗。那是因為沒有任何測試可執行!

此時,Vitest 已設定完成,您可以開始考慮編寫單元測試。

不需要測試的檔案

在直接開始編寫測試之前,您將先看看不需要測試的檔案,並思考為什麼。

以下是不需要測試的檔案清單

  • src/index.ts
  • src/auth/auth.router.ts
  • src/auth/auth.schemas.ts
  • src/quotes/quotes.router.ts
  • src/quotes/quotes.schemas.ts
  • src/quotes/quotes.service.ts
  • src/lib/prisma.ts
  • src/lib/createServer.ts

這些檔案沒有任何需要單元測試的自訂行為。

在接下來的兩個章節中,您將了解這些檔案中導致它們不需要測試的兩個主要情境。

檔案沒有自訂行為

看看應用程式中的下列範例

src/quotes/quotes.router.ts 中,實際發生的唯一事情是叫用 Express 框架提供的函數。有一些自訂函數(validateQuoteController.*)在運作,但這些函數在不同的檔案中定義,並將在其自身的環境中進行測試。

第二個檔案 src/auth/auth.schemas.ts 非常相似。雖然此檔案對於應用程式很重要,但實際上沒有任何需要測試的內容。程式碼只是匯出使用外部模組 zod 定義的結構描述。

函數僅叫用外部模組

另一個需要指出的情境是 src/quotes/quotes.service.ts 中的情境

此服務匯出兩個函數。這兩個函數都封裝了 Prisma Client 函數叫用並傳回結果。

正如本文先前所述,不需要測試外部程式碼。因此,可以略過此檔案。

如果您看看上面不需要測試的清單中的其餘檔案,您會發現每個檔案都不需要測試,原因在此處概述。

您將測試的內容

專案中其餘的 .ts 檔案都包含應進行單元測試的功能。需要測試的完整檔案清單如下

  • src/auth/auth.controller.ts
  • src/auth/auth.service.ts
  • src/lib/middlewares.ts
  • src/lib/utility-classes.ts
  • src/quotes/quotes.controller.ts
  • src/quotes/tags.service.ts

這些檔案中的每個函數都應提供其自身的測試套件,以驗證其行為是否正確。

您可以想像,這可能會產生大量測試!為了將其量化,Express API 包含十三個不同的函數,需要進行測試,並且每個函數都可能有一個包含兩個以上測試的套件。這表示至少有二十六個測試需要編寫!

為了使本文的長度易於管理,您將編寫單一檔案 src/quotes/tags.service.ts 的測試,因為此檔案的測試涵蓋了本文希望涵蓋的所有重要單元測試概念。

注意:如果您對此 API 的完整測試集的外觀感到好奇,Github 儲存庫的 unit-tests 分支具有每個函數的完整測試集。

測試標籤服務

標籤服務匯出兩個函數,upsertTagsdeleteOrphanedTags

首先,在與 tags.service.ts 相同的目錄中建立一個名為 tags.service.test.ts 的新檔案

注意:有很多種組織測試的方法。在本系列中,測試將編寫在緊鄰測試目標的檔案中,也稱為並置測試。

如果您使用 VSCode 並且擁有 v1.64 或更高版本,您可以使用一個很酷的功能,可以在並置測試及其目標時清理專案的檔案樹狀結構。

在 VSCode 中,前往螢幕頂部選項列中的程式碼 > 喜好設定 > 設定

在設定頁面中,輸入 檔案巢狀結構 來搜尋檔案巢狀結構設定。啟用下面的設定

File nesting option in VSCode

接下來,在這些設定中向下捲動一點,您會看到 檔案總管 > 檔案巢狀結構:模式 區段。

如果名為 *.ts 的項目不存在,請建立一個。然後將 *.ts 項目值更新為 ${capture}.*.ts

File nesting setting in VSCode

這讓 VSCode 可以將任何檔案巢狀結構化在名為 ${capture}.ts 的主要檔案下。為了更好地說明,請參閱下列範例

Nested files

在上面,您可以看到一個名為 quotes.controller.ts 的檔案。巢狀結構化在該檔案下的是 quotes.controller.test.ts。雖然不是絕對必要,但此設定可能有助於在並置單元測試時稍微清理您的檔案樹狀結構。

匯入必要的模組

在新的 tags.service.test.ts 檔案的頂部,您需要匯入一些東西,以便您可以編寫測試

以下是這些匯入中的每一個將用於的用途

  • TagsService:這是您正在編寫測試的服務。您需要匯入它,以便您可以叫用其函數。
  • prismaMock:這是 lib/__mocks__/prisma 提供的 Prisma Client 的模擬版本。
  • randomColorupsertTags 函數中用於產生隨機顏色的程式庫。
  • describevitest 提供的函數,可讓您描述測試套件。

需要注意的是 prismaMock 匯入。這是模擬的 Prisma Client 執行個體,可讓您執行 prisma 查詢,而無需實際點擊資料庫。由於它是模擬的,您也可以操縱查詢回應並監視其方法。

注意:如果您不確定 prismaMock 匯入是什麼以及它是如何運作的,請務必閱讀本系列中的上一篇文章,其中說明了此模組的角色。

描述測試套件

您現在可以使用 Vitest 提供的 describe 函數來描述這組特定的測試

這會在輸出測試結果時,將此檔案中的測試分組到一個區段中,以便更容易查看哪些套件通過和失敗。

模擬目標檔案使用的任何模組

在編寫實際測試套件之前的最後一件事是模擬 tags.service.ts 檔案中使用的外部模組。這將讓您能夠控制這些模組的輸出,並確保您的測試不會受到外部程式碼的污染。

在此服務中,有兩個模組要模擬:PrismaClientrandomColor

透過新增以下內容來模擬這些模組

在上面,lib/prisma 模組是使用 Vitest 的自動模擬偵測演算法模擬的,該演算法在與「真實」Prisma 模組相同的目錄中尋找名為 __mocks__ 的資料夾和 __mocks__/prisma.ts 檔案。此檔案的匯出用於取代真實模組匯出的模擬模組。

randomColor 模擬有點不同,因為模組僅匯出預設值,這是一個函數。vi.mock 的第二個參數是一個函數,該函數傳回模組在匯入時應傳回的物件。上面的程式碼片段將 default 金鑰新增至此物件,並將其值設定為傳回值為 '#ffffff' 的可監視函數。

在測試套件的環境中,beforeEachvi.restoreAllMocks 用於確保在每個個別測試之間,模擬都會還原為其原始狀態。這很重要,因為在某些測試中,您將修改該特定測試的模擬行為。

注意:如果您不確定這些模擬如何運作,請務必參考本系列中的上一篇文章,其中涵蓋了模擬。

每當在 TagsService 中匯入這些模組時,現在將匯入模擬版本。

測試 upsertTags 函數

upsertTags 函數接受標籤名稱陣列,並為每個名稱建立一個新標籤。但是,如果資料庫中現有的標籤具有相同的名稱,則它不會建立標籤。函數的傳回值是與提供給函數的所有標籤名稱(新的和現有的)相關聯的標籤 ID 陣列。

在測試套件中 beforeEach 叫用的正下方,新增另一個 describe 以描述與 upsertTags 函數相關的測試套件。同樣,這樣做是為了對測試的輸出進行分組,使其易於查看與此特定函數相關的哪些測試通過。

現在是時候決定您編寫的測試應涵蓋哪些內容。查看 upsertTags 函數,考慮它有哪些特定行為。每個所需的行為都應進行測試。

下面,新增了註解,顯示應在此函數中測試的每個行為。註解已編號,表示測試將編寫的順序

有了要測試的情境清單,您現在可以開始為每個情境編寫測試。

驗證函數傳回標籤 ID 清單

第一個測試將確保函數的傳回值是標籤 ID 陣列。在用於此函數的 describe 區塊中,新增新的測試

上面的測試執行以下操作

  1. 模擬 Prisma Client 的 $transaction 函數的回應
  2. 叫用 upsertTags 函數
  3. 確保函數的回應等於 $transaction 的預期模擬回應

此測試很重要,因為它專門測試函數的所需結果。如果此函數在未來發生變更,此測試可確保函數的結果保持在預期範圍內。

注意:如果您不確定 Vitest 提供的特定方法用途為何,請參閱 Vitest 的文件

如果您現在執行 npm run test:unit,您應該會看到您的測試成功通過。

驗證函數僅建立尚不存在的標籤

上述計劃的下一個測試將驗證函數不會在資料庫中建立重複的標籤。

此函數會收到代表標籤名稱的字串列表。此函數首先檢查是否已存在具有這些名稱的標籤,並根據結果篩選,僅建立新的標籤。

此測試應該

  • 模擬首次調用 prisma.tag.findMany 以回傳單一標籤。這表示根據提供給函數的名稱找到一個現有標籤。
  • 使用三個標籤名稱調用 upsertTags。其中一個名稱應該是 tag1,即模擬的現有標籤的名稱。
  • 確保 prisma.tag.createMany 僅收到與 tag1 不符的兩個標籤。

upsertTags 函數的 describe 區塊中,在前一個測試下方新增以下測試

再次執行 npm run test:unit 現在應該會顯示您的兩個測試皆通過。

驗證函數為新標籤提供隨機顏色

在下一個測試中,您需要驗證每當建立新標籤時,都會為其提供新的隨機顏色。

為了做到這一點,請編寫一個基本測試,插入三個新標籤。在調用 upsertTags 函數之後,您可以確保 randomColor 函數被調用了三次。

以下程式碼片段顯示了此測試應有的樣子。在您先前於 upsertTags 函數的 describe 區塊中編寫的測試下方新增此新測試

npm run test:unit 命令應產生三個成功的測試。

您可能想知道上面的測試是如何檢查 randomColor 被調用多少次的。

請記住,在此檔案的上下文中,randomColor 模組已被模擬,且其預設導出被配置為 vi.fn,其提供一個回傳靜態字串值的函數。

由於使用了 vi.fn,模擬函數現在已在 Vitest 中註冊為您可以監視的函數。

因此,您可以存取特殊屬性,例如在目前測試期間函數被調用的次數計數。

驗證函數在其回傳陣列中包含新建立的標籤 ID

在此測試中,您需要驗證函數回傳與提供給函數的每個標籤名稱相關聯的標籤 ID。這表示它應該回傳現有的標籤 ID 以及任何新建立的標籤的 ID。

此測試應該

  1. 導致首次調用 tag.findMany 以回傳標籤,以模擬找到現有標籤
  2. 模擬 tag.createMany 的回應
  3. 導致第二次調用 tag.findMany 以回傳兩個標籤,表示它找到了兩個新建立的標籤
  4. 使用三個標籤調用 upsertTags 函數
  5. 確保回傳所有三個 ID

新增以下測試以完成此操作

執行 npm run test:unit 以驗證上述測試是否有效。

驗證當未提供任何標籤名稱時,函數回傳空陣列

正如您可能預期的,如果未向此函數提供任何標籤名稱,則它應該無法回傳任何標籤 ID。

在此測試中,新增以下內容以驗證此行為是否正常運作

有了這個,此函數已確定的所有情境都已測試完畢!

如果您使用您新增到 package.json 的任何一個腳本執行測試,您應該會看到所有測試都執行並成功通過!

注意:如果您尚未執行此命令,系統可能會提示您安裝 @vitest/ui 套件並重新執行命令。

Successful suite of tests

測試 deleteOrphanedTags 函數

此函數與先前的函數情境非常不同。

正如您可能已經確定的,此函數僅僅封裝了 Prisma Client 函數的調用。因此…您猜對了!此函數實際上不需要測試!

總結 & 接下來的步驟

在本篇文章的過程中,您

  • 了解了單元測試是什麼以及它對您的應用程式的重要性
  • 看到了一些單元測試並非絕對必要的狀況範例
  • 設定 Vitest
  • 學到了一些在編寫測試時讓生活更輕鬆的技巧
  • 親自嘗試為 API 中的服務編寫單元測試

雖然本文僅涵蓋了 quotes API 中的一個檔案,但用於測試 tags 服務的概念和方法也適用於應用程式的其餘部分。我鼓勵您為 API 的其餘部分編寫測試以進行練習!

在本系列的下一部分,您將深入研究整合測試,並為同一個應用程式編寫整合測試。

不要錯過下一篇文章!

註冊 Prisma 電子報