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訪問豐富的社群構建模組和包生態系統。使用外部庫可以節省大量時間,否則您可能需要重新發明輪子。

雖然使用外部模組沒有錯,但在考慮測試使用這些模組的函式時,需要考慮一些事項。最重要的是,務必記住這一點

如果您不信任某個外部包,並認為應該對其編寫測試,那麼您可能不應該使用該特定包。

以下面的函式為例

此函式接收正方形一邊的長度,並返回一個包含更明確的正方形的物件,包括一個獨特的顏色。

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

  • 當提供小於1的數字時,函式返回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的變數,您可以將其值設定為任意string,以及一個名為DATABASE_URL的變數,該變數目前可以留空

.env檔案中,API_SECRET變數提供了一個由認證服務用於加密密碼的金鑰。在實際應用程式中,此值應替換為包含數字和字母字元的長時間隨機字串。

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

最後,您需要根據Prisma schema生成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定義的schema。

函式只調用外部模組

另一個需要指出的場景是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搜尋檔案巢狀設定。啟用以下設定

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的模擬版本。
  • randomColor:在upsertTags函式中用於生成隨機顏色的庫。
  • describe:一個由vitest提供的函式,允許您描述一個測試套件。

需要注意的是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中的一個檔案,但用於測試標籤服務的概念和方法也適用於應用程式的其餘部分。我鼓勵您為API的其餘部分編寫測試進行練習!

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

不要錯過下一篇文章!

訂閱Prisma新聞通訊

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