单元测试
单元测试旨在隔离一小部分(单元)代码并测试其逻辑上可预测的行为。它通常涉及模拟对象或服务器响应以模拟真实世界行为。单元测试的一些好处包括
- 快速查找并隔离代码中的错误。
- 通过指示某些代码块应该做什么来为每个代码模块提供文档。
- 一个有用的指标,表明重构进展顺利。代码重构后,测试应该仍然通过。
在 Prisma ORM 的上下文中,这通常意味着测试使用 Prisma 客户端进行数据库调用的函数。
单个测试应该专注于您的函数逻辑如何处理不同的输入(例如空值或空列表)。
这意味着您应该尽可能减少依赖项,例如外部服务和数据库,以使测试及其环境尽可能轻量级。
注意:这篇博文提供了在您的 Express 项目中使用 Prisma ORM 实现单元测试的全面指南。如果您想深入研究此主题,请务必阅读它!
先决条件
本指南假定您已经在您的项目中设置了 JavaScript 测试库Jest
和ts-jest
。
模拟 Prisma 客户端
为了确保您的单元测试与外部因素隔离,您可以模拟 Prisma 客户端,这意味着您可以享受使用模式(类型安全)的好处,而无需在运行测试时实际调用数据库。
本指南将介绍两种模拟 Prisma 客户端的方法:单例实例和依赖项注入。两种方法都有其优点,具体取决于您的用例。为了帮助模拟 Prisma 客户端,我们将使用jest-mock-extended
包。
npm install [email protected] --save-dev
在撰写本文时,本指南使用的是 jest-mock-extended
版本 ^2.0.4
。
单例
以下步骤指导您使用单例模式模拟 Prisma 客户端。
-
在您的项目根目录中创建一个名为
client.ts
的文件,并添加以下代码。这将实例化 Prisma 客户端实例。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 客户端实例),并使用 jest-mock-extended
中的 mockDeep
方法来启用对 Prisma 客户端上可用对象和方法的访问。然后,它在每个测试运行之前重置模拟的实例。
接下来,将 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 客户端看到循环依赖错误,请尝试将 "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 客户端进行模拟调用并运行要测试的查询。ctx
上下文将用于运行要针对其进行测试的场景查询。
示例单元测试
使用 Prisma ORM 进行单元测试的一个实际用例可能是注册表单。您的用户填写一个表单,该表单会调用一个函数,该函数会使用 Prisma 客户端对您的数据库进行调用。
以下所有示例都使用以下模式模型
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
acceptTermsAndConditions Boolean
}
以下单元测试将模拟以下过程
- 创建新用户
- 更新用户的姓名
- 如果未接受条款,则创建用户失败
使用依赖项注入模式的函数将被注入上下文(作为参数传递)到它们,而使用单例模式的函数将使用 Prisma 客户端的单例实例。
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 客户端的使用方式。
依赖项注入示例将上下文传递到要测试的函数中,并使用它来调用模拟实现。
单例示例使用单例客户端实例来调用模拟实现。
import { createUser, updateUsername } from '../functions-without-context'
import { prismaMock } from '../singleton'
test('should create new user ', async () => {
const user = {
id: 1,
name: 'Rich',
email: '[email protected]',
acceptTermsAndConditions: true,
}
prismaMock.user.create.mockResolvedValue(user)
await expect(createUser(user)).resolves.toEqual({
id: 1,
name: 'Rich',
email: '[email protected]',
acceptTermsAndConditions: true,
})
})
test('should update a users name ', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: '[email protected]',
acceptTermsAndConditions: true,
}
prismaMock.user.update.mockResolvedValue(user)
await expect(updateUsername(user)).resolves.toEqual({
id: 1,
name: 'Rich Haines',
email: '[email protected]',
acceptTermsAndConditions: true,
})
})
test('should fail if user does not accept terms', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: '[email protected]',
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: '[email protected]',
acceptTermsAndConditions: true,
}
mockCtx.prisma.user.create.mockResolvedValue(user)
await expect(createUser(user, ctx)).resolves.toEqual({
id: 1,
name: 'Rich',
email: '[email protected]',
acceptTermsAndConditions: true,
})
})
test('should update a users name ', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: '[email protected]',
acceptTermsAndConditions: true,
}
mockCtx.prisma.user.update.mockResolvedValue(user)
await expect(updateUsername(user, ctx)).resolves.toEqual({
id: 1,
name: 'Rich Haines',
email: '[email protected]',
acceptTermsAndConditions: true,
})
})
test('should fail if user does not accept terms', async () => {
const user = {
id: 1,
name: 'Rich Haines',
email: '[email protected]',
acceptTermsAndConditions: false,
}
mockCtx.prisma.user.create.mockImplementation()
await expect(createUser(user, ctx)).resolves.toEqual(
new Error('User must accept terms!')
)
})