單元測試
單元測試旨在隔離一小部分(單元)程式碼,並針對邏輯上可預測的行為進行測試。它通常涉及模擬物件或伺服器回應,以模擬真實世界的行為。單元測試的一些好處包括:
- 快速找到並隔離程式碼中的錯誤。
- 透過指示特定程式碼區塊應執行的操作,為每個程式碼模組提供文件。
- 有助於衡量重構是否順利。程式碼重構後,測試應仍然通過。
在 Prisma ORM 的背景下,這通常意味著測試一個使用 Prisma Client 進行資料庫呼叫的函式。
單一測試應著重於您的函式邏輯如何處理不同的輸入(例如空值或空列表)。
這表示您應盡可能移除依賴項,例如外部服務和資料庫,以保持測試及其環境盡可能輕量。
注意:這篇部落格文章提供了在您的 Express 專案中使用 Prisma ORM 實作單元測試的完整指南。如果您想深入探討這個主題,請務必閱讀它!
先決條件
本指南假設您的專案中已設定 JavaScript 測試函式庫 Jest
和 ts-jest
。
模擬 Prisma Client
為了確保您的單元測試與外部因素隔離,您可以模擬 Prisma Client。這表示您可以享受使用 schema(型別安全)的好處,而無需在執行測試時實際呼叫您的資料庫。
本指南將介紹兩種模擬 Prisma Client 的方法:單例模式實例和依賴注入。兩者都有其優點,具體取決於您的使用案例。為了協助模擬 Prisma Client,將使用 jest-mock-extended
套件。
npm install jest-mock-extended@2.0.4 --save-dev
在撰寫本文時,本指南使用 jest-mock-extended
版本 ^2.0.4
。
單例模式
以下步驟將引導您使用單例模式模擬 Prisma Client。
-
在您的專案根目錄建立一個名為 `client.ts` 的檔案,並新增以下程式碼。這將實例化一個 Prisma Client 實例。
client.tsimport { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prisma -
接下來,在您的專案根目錄建立一個名為 `singleton.ts` 的檔案,並新增以下內容
singleton.tsimport { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import prisma from './client'
jest.mock('./client', () => ({
__esModule: true,
default: mockDeep<PrismaClient>(),
}))
beforeEach(() => {
mockReset(prismaMock)
})
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
singleton 檔案告知 Jest 模擬預設匯出 (./client.ts 中的 Prisma Client 實例),並使用 `jest-mock-extended` 中的 `mockDeep` 方法來啟用對 Prisma Client 上可用物件和方法的存取。然後,它會在每次測試執行前重設模擬的實例。
接下來,將 `setupFilesAfterEnv` 屬性新增至您的 `jest.config.js` 檔案,並指定 `singleton.ts` 檔案的路徑。
module.exports = {
clearMocks: true,
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/singleton.ts'],
}
依賴注入
另一種可以使用的常見模式是依賴注入。
-
建立一個 `context.ts` 檔案並新增以下內容
context.tsimport { PrismaClient } from '@prisma/client'
import { mockDeep, DeepMockProxy } from 'jest-mock-extended'
export type Context = {
prisma: PrismaClient
}
export type MockContext = {
prisma: DeepMockProxy<PrismaClient>
}
export const createMockContext = (): MockContext => {
return {
prisma: mockDeep<PrismaClient>(),
}
}
如果您發現透過模擬 Prisma Client 突顯了循環依賴錯誤,請嘗試將 "strictNullChecks": true
新增至您的 tsconfig.json
。
-
若要使用 context,您可以在您的測試檔案中執行以下操作
import { MockContext, Context, createMockContext } from '../context'
let mockCtx: MockContext
let ctx: Context
beforeEach(() => {
mockCtx = createMockContext()
ctx = mockCtx as unknown as Context
})
這會在每次測試執行前透過 createMockContext
函式建立新的 context。此 (mockCtx) context 將用於對 Prisma Client 進行模擬呼叫並執行查詢以進行測試。ctx
context 將用於執行針對其進行測試的情境查詢。
單元測試範例
單元測試 Prisma ORM 的真實世界使用案例可能是註冊表單。您的使用者填寫表單,該表單呼叫一個函式,而該函式又使用 Prisma Client 來呼叫您的資料庫。
以下所有範例都使用以下 schema 模型
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
acceptTermsAndConditions Boolean
}
以下單元測試將模擬以下流程:
- 建立新使用者
- 更新使用者名稱
- 如果未接受條款,則無法建立使用者
使用依賴注入模式的函式將把 context 注入(作為參數傳遞)到其中,而使用單例模式的函式將使用 Prisma Client 的單例模式實例。
import { Context } from './context'
interface CreateUser {
name: string
email: string
acceptTermsAndConditions: boolean
}
export async function createUser(user: CreateUser, ctx: Context) {
if (user.acceptTermsAndConditions) {
return await ctx.prisma.user.create({
data: user,
})
} else {
return new Error('User must accept terms!')
}
}
interface UpdateUser {
id: number
name: string
email: string
}
export async function updateUsername(user: UpdateUser, ctx: Context) {
return await ctx.prisma.user.update({
where: { id: user.id },
data: user,
})
}
import prisma from './client'
interface CreateUser {
name: string
email: string
acceptTermsAndConditions: boolean
}
export async function createUser(user: CreateUser) {
if (user.acceptTermsAndConditions) {
return await prisma.user.create({
data: user,
})
} else {
return new Error('User must accept terms!')
}
}
interface UpdateUser {
id: number
name: string
email: string
}
export async function updateUsername(user: UpdateUser) {
return await prisma.user.update({
where: { id: user.id },
data: user,
})
}
每個方法的測試都相當相似,差異在於模擬 Prisma Client 的使用方式。
**依賴注入**範例將 context 傳遞到正在測試的函式,並使用它來呼叫模擬實作。
**單例模式**範例使用單例模式 client 實例來呼叫模擬實作。
import { createUser, updateUsername } from '../functions-without-context'
import { prismaMock } from '../singleton'
test('should create new user ', async () => {
const user = {
id: 1,
name: 'Rich',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
}
prismaMock.user.create.mockResolvedValue(user)
await expect(createUser(user)).resolves.toEqual({
id: 1,
name: 'Rich',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
})
})
test('should update a users name ', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
}
prismaMock.user.update.mockResolvedValue(user)
await expect(updateUsername(user)).resolves.toEqual({
id: 1,
name: 'Rich Haines',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
})
})
test('should fail if user does not accept terms', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: 'hello@prisma.io',
acceptTermsAndConditions: false,
}
prismaMock.user.create.mockImplementation()
await expect(createUser(user)).resolves.toEqual(
new Error('User must accept terms!')
)
})
import { MockContext, Context, createMockContext } from '../context'
import { createUser, updateUsername } from '../functions-with-context'
let mockCtx: MockContext
let ctx: Context
beforeEach(() => {
mockCtx = createMockContext()
ctx = mockCtx as unknown as Context
})
test('should create new user ', async () => {
const user = {
id: 1,
name: 'Rich',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
}
mockCtx.prisma.user.create.mockResolvedValue(user)
await expect(createUser(user, ctx)).resolves.toEqual({
id: 1,
name: 'Rich',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
})
})
test('should update a users name ', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
}
mockCtx.prisma.user.update.mockResolvedValue(user)
await expect(updateUsername(user, ctx)).resolves.toEqual({
id: 1,
name: 'Rich Haines',
email: 'hello@prisma.io',
acceptTermsAndConditions: true,
})
})
test('should fail if user does not accept terms', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: 'hello@prisma.io',
acceptTermsAndConditions: false,
}
mockCtx.prisma.user.create.mockImplementation()
await expect(createUser(user, ctx)).resolves.toEqual(
new Error('User must accept terms!')
)
})