單元測試涉及測試個別、隔離的程式碼單元,以確保它們如預期般運作。在本文中,您將學習如何識別程式碼庫中應進行單元測試的區域、如何編寫這些測試,以及如何處理針對使用 Prisma Client 的函數的測試。
目錄
簡介
單元測試是確保應用程式中個別程式碼單元(例如函數)如您預期運作的主要方法之一。
對於剛接觸測試的人來說,要理解什麼是單元測試可能非常困難。他們不僅必須了解其應用程式的運作方式、如何編寫測試以及如何準備測試環境,而且還必須了解他們應該測試什麼!
因此,開發人員通常會採用這種測試方法
注意:感謝 @RoxCodes 的誠實 😉
在本系列中,您將使用一個功能完整的應用程式。其程式碼庫中唯一缺少的是一套驗證其運作是否正常的測試。
在本系列課程中,您將考慮程式碼的各個領域,並逐步了解應該測試什麼、為什麼需要測試以及如何編寫這些測試。這將包括單元測試、整合測試、端對端測試,以及設定執行這些測試的持續整合 (CI) 和持續開發 (CD) 工作流程。
在本文中,您將特別深入研究程式碼的特定區域,並針對它們編寫單元測試,以確保這些區域的個別建構區塊運作正常。
什麼是單元測試?
單元測試是一種針對小型、隔離的程式碼片段編寫測試的測試類型。單元測試的目標是小型程式碼單元,以確保它們在各種情況下如預期般運作。
通常,單元測試會以個別 函數
為目標,因為函數通常是 JavaScript 應用程式中最小的單一程式碼單元。
以下列函數為例
雖然這個函數很簡單,但它是單元測試的良好候選者。它包含一組單一的功能,並封裝在一個函數中。為了確保此函數運作正常,您可以向其提供字串 'abcde'
,並確保傳回字串 'edcba'
。
相關的套件測試或測試集可能如下所示
正如您可能已在上面注意到的,單元測試的目標只是確保應用程式中最小的建構區塊運作正常。透過這樣做,您可以建立信心,隨著您開始組合這些建構區塊,產生的行為是可預測的。
之所以如此重要的原因是如上所示。當您執行單元測試時,如果所有測試都通過,您可以確定每個建構區塊都運作正常,因此,您的應用程式也如預期般運作。但是,如果即使一個測試失敗,您也可以假設您的應用程式運作不正常,並且您可以根據失敗的測試準確地知道問題所在。
什麼不是單元測試?
在單元測試中,目標是確保您的自訂程式碼如預期般運作。從上一個句子中需要注意的重點是「自訂程式碼」這個詞組。
作為 JavaScript 開發人員,您可以透過 npm 存取由社群建立的豐富模組和套件生態系統。使用外部程式庫可讓您節省大量時間,否則您可能會重新發明輪子。
雖然使用外部模組沒有什麼問題,但在考慮測試使用這些模組的函數時,仍有一些注意事項需要考慮。最重要的是,請記住這一點
如果您不信任外部套件,並且認為應該針對它編寫測試,那麼您可能不應該使用該特定套件。
以下列函數為例
此函數接受正方形一邊的長度,並傳回一個包含更精確定義的正方形的物件,包括正方形的獨特顏色。
在為上述函數編寫單元測試時,您可能需要驗證以下內容
- 當提供的數字小於一時,函數傳回
null
- 函數正確計算面積
- 函數傳回具有正確值的正確形狀的物件
randomColor
函數被呼叫一次
請注意,沒有提到測試以確保每個正方形實際上都獲得獨特的顏色。這是因為 randomColor
假定可以正常運作,因為它是一個外部模組。
注意:無論
randomColor
是透過 npm 套件提供,還是甚至是另一個檔案中的自訂建構函數,都應假定它在此環境中運作正常。如果randomColor
是您在另一個檔案中編寫的函數,則應在其自身的隔離環境中進行測試。想想「建構區塊」!
這個概念很重要,因為它也適用於 Prisma Client。在您的應用程式中使用 Prisma 時,Prisma Client 是一個外部模組。因此,任何測試都應假定您的用戶端提供的函數如預期般運作。
您將使用的技術
先決條件
假設知識
以下知識對於開始本系列課程會很有幫助
- JavaScript 或 TypeScript 的基本知識
- Prisma Client 及其功能的基礎知識
- 有一些 Express 的經驗會很好
開發環境
若要跟著提供的範例進行操作,您需要具備
本系列將大量使用這個 GitHub 儲存庫。請務必複製儲存庫並簽出 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。
首先,使用下列命令安裝 vitest
和 vitest-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.json
的 scripts
區段
上面新增了兩個新的指令碼
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 框架提供的函數。有一些自訂函數(validate
和 QuoteController.*
)在運作,但這些函數在不同的檔案中定義,並將在其自身的環境中進行測試。
第二個檔案 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
分支具有每個函數的完整測試集。
測試標籤服務
標籤服務匯出兩個函數,upsertTags
和 deleteOrphanedTags
。
首先,在與 tags.service.ts
相同的目錄中建立一個名為 tags.service.test.ts
的新檔案
注意:有很多種組織測試的方法。在本系列中,測試將編寫在緊鄰測試目標的檔案中,也稱為並置測試。
如果您使用 VSCode 並且擁有 v1.64 或更高版本,您可以使用一個很酷的功能,可以在並置測試及其目標時清理專案的檔案樹狀結構。
在 VSCode 中,前往螢幕頂部選項列中的程式碼 > 喜好設定 > 設定。
在設定頁面中,輸入 檔案巢狀結構
來搜尋檔案巢狀結構設定。啟用下面的設定

接下來,在這些設定中向下捲動一點,您會看到 檔案總管 > 檔案巢狀結構:模式 區段。
如果名為 *.ts 的項目不存在,請建立一個。然後將 *.ts 項目值更新為 ${capture}.*.ts

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

在上面,您可以看到一個名為 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
檔案中使用的外部模組。這將讓您能夠控制這些模組的輸出,並確保您的測試不會受到外部程式碼的污染。
在此服務中,有兩個模組要模擬:PrismaClient
和 randomColor
。
透過新增以下內容來模擬這些模組
在上面,lib/prisma
模組是使用 Vitest 的自動模擬偵測演算法模擬的,該演算法在與「真實」Prisma 模組相同的目錄中尋找名為 __mocks__
的資料夾和 __mocks__/prisma.ts
檔案。此檔案的匯出用於取代真實模組匯出的模擬模組。
randomColor
模擬有點不同,因為模組僅匯出預設值,這是一個函數。vi.mock
的第二個參數是一個函數,該函數傳回模組在匯入時應傳回的物件。上面的程式碼片段將 default
金鑰新增至此物件,並將其值設定為傳回值為 '#ffffff'
的可監視函數。
在測試套件的環境中,beforeEach
和 vi.restoreAllMocks
用於確保在每個個別測試之間,模擬都會還原為其原始狀態。這很重要,因為在某些測試中,您將修改該特定測試的模擬行為。
注意:如果您不確定這些模擬如何運作,請務必參考本系列中的上一篇文章,其中涵蓋了模擬。
每當在 TagsService
中匯入這些模組時,現在將匯入模擬版本。
測試 upsertTags
函數
upsertTags
函數接受標籤名稱陣列,並為每個名稱建立一個新標籤。但是,如果資料庫中現有的標籤具有相同的名稱,則它不會建立標籤。函數的傳回值是與提供給函數的所有標籤名稱(新的和現有的)相關聯的標籤 ID 陣列。
在測試套件中 beforeEach
叫用的正下方,新增另一個 describe
以描述與 upsertTags
函數相關的測試套件。同樣,這樣做是為了對測試的輸出進行分組,使其易於查看與此特定函數相關的哪些測試通過。
現在是時候決定您編寫的測試應涵蓋哪些內容。查看 upsertTags
函數,考慮它有哪些特定行為。每個所需的行為都應進行測試。
下面,新增了註解,顯示應在此函數中測試的每個行為。註解已編號,表示測試將編寫的順序
有了要測試的情境清單,您現在可以開始為每個情境編寫測試。
驗證函數傳回標籤 ID 清單
第一個測試將確保函數的傳回值是標籤 ID 陣列。在用於此函數的 describe
區塊中,新增新的測試
上面的測試執行以下操作
- 模擬 Prisma Client 的
$transaction
函數的回應 - 叫用
upsertTags
函數 - 確保函數的回應等於
$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。
此測試應該
- 導致首次調用
tag.findMany
以回傳標籤,以模擬找到現有標籤 - 模擬
tag.createMany
的回應 - 導致第二次調用
tag.findMany
以回傳兩個標籤,表示它找到了兩個新建立的標籤 - 使用三個標籤調用
upsertTags
函數 - 確保回傳所有三個 ID
新增以下測試以完成此操作
執行 npm run test:unit
以驗證上述測試是否有效。
驗證當未提供任何標籤名稱時,函數回傳空陣列
正如您可能預期的,如果未向此函數提供任何標籤名稱,則它應該無法回傳任何標籤 ID。
在此測試中,新增以下內容以驗證此行為是否正常運作
有了這個,此函數已確定的所有情境都已測試完畢!
如果您使用您新增到 package.json
的任何一個腳本執行測試,您應該會看到所有測試都執行並成功通過!
注意:如果您尚未執行此命令,系統可能會提示您安裝
@vitest/ui
套件並重新執行命令。

測試 deleteOrphanedTags
函數
此函數與先前的函數情境非常不同。
正如您可能已經確定的,此函數僅僅封裝了 Prisma Client 函數的調用。因此…您猜對了!此函數實際上不需要測試!
總結 & 接下來的步驟
在本篇文章的過程中,您
- 了解了單元測試是什麼以及它對您的應用程式的重要性
- 看到了一些單元測試並非絕對必要的狀況範例
- 設定 Vitest
- 學到了一些在編寫測試時讓生活更輕鬆的技巧
- 親自嘗試為 API 中的服務編寫單元測試
雖然本文僅涵蓋了 quotes API 中的一個檔案,但用於測試 tags 服務的概念和方法也適用於應用程式的其餘部分。我鼓勵您為 API 的其餘部分編寫測試以進行練習!
在本系列的下一部分,您將深入研究整合測試,並為同一個應用程式編寫整合測試。
不要錯過下一篇文章!
註冊 Prisma 電子報