單元測試
單元測試旨在隔離一小部分(單元)程式碼並測試其邏輯上可預測的行為。它通常涉及模擬物件或伺服器響應以模擬真實世界的行為。單元測試的一些好處包括:
- 快速發現和隔離程式碼中的錯誤。
- 透過指示特定程式碼塊應執行的操作,為每個程式碼模組提供文件。
- 衡量重構是否順利的有用指標。程式碼重構後,測試仍應透過。
在 Prisma ORM 的語境中,這通常意味著測試使用 Prisma Client 進行資料庫呼叫的函式。
單個測試應側重於您的函式邏輯如何處理不同的輸入(例如空值或空列表)。
這意味著您應儘可能移除依賴項,例如外部服務和資料庫,以保持測試及其環境儘可能輕量。
注意:這篇部落格文章提供了一份在 Express 專案中使用 Prisma ORM 實現單元測試的全面指南。如果您想深入探討此主題,請務必閱讀!
先決條件
本指南假定您已在專案中設定了 JavaScript 測試庫 Jest 和 ts-jest。
模擬 Prisma Client
為確保您的單元測試與外部因素隔離,您可以模擬 Prisma Client,這意味著您可以利用模式的優勢(型別安全),而無需在測試執行時實際呼叫資料庫。
本指南將涵蓋兩種模擬 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>
單例檔案告訴 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 檔案中。
-
要在測試檔案中使用上下文,您可以這樣做:
import { MockContext, Context, createMockContext } from '../context'
let mockCtx: MockContext
let ctx: Context
beforeEach(() => {
mockCtx = createMockContext()
ctx = mockCtx as unknown as Context
})
這將透過 createMockContext 函式在每次測試執行前建立一個新的上下文。此 (mockCtx) 上下文將用於對 Prisma Client 進行模擬呼叫並執行一個要測試的查詢。ctx 上下文將用於執行一個將與測試進行比較的場景查詢。
單元測試示例
單元測試 Prisma ORM 的一個真實用例可能是登錄檔單。您的使用者填寫一個表單,該表單呼叫一個函式,該函式又使用 Prisma Client 對您的資料庫進行呼叫。
所有以下示例都使用以下模式模型
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
acceptTermsAndConditions Boolean
}
以下單元測試將模擬以下過程:
- 建立一個新使用者
- 更新使用者名稱稱
- 如果條款未被接受,則建立使用者失敗
使用依賴注入模式的函式將注入上下文(作為引數傳入),而使用單例模式的函式將使用 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。
依賴注入 示例將上下文傳遞給正在測試的函式,並使用它呼叫模擬實現。
單例 示例使用單例客戶端例項來呼叫模擬實現。
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!')
)
})