单元测试
单元测试旨在隔离一小部分(单元)代码,并针对逻辑上可预测的行为对其进行测试。它通常涉及模拟对象或服务器响应,以模拟真实世界的行为。单元测试的一些好处包括
- 快速查找和隔离代码中的错误。
- 通过指示某些代码块应该做什么,为每个代码模块提供文档。
- 衡量重构是否顺利的有用指标。在代码重构后,测试应该仍然通过。
在 Prisma ORM 的上下文中,这通常意味着测试一个使用 Prisma Client 进行数据库调用的函数。
单个测试应侧重于您的函数逻辑如何处理不同的输入(例如空值或空列表)。
这意味着您应该尽量消除尽可能多的依赖项,例如外部服务和数据库,以使测试及其环境尽可能轻量级。
注意:这篇博客文章提供了关于在您的 Express 项目中使用 Prisma ORM 实现单元测试的全面指南。如果您希望深入研究这个主题,请务必阅读它!
先决条件
本指南假设您的项目中已经设置了 JavaScript 测试库 Jest
和 ts-jest
。
模拟 Prisma Client
为了确保您的单元测试与外部因素隔离,您可以模拟 Prisma Client。这意味着您可以获得使用模式(类型安全)的好处,而无需在运行测试时实际调用数据库。
本指南将介绍模拟 Prisma Client 的两种方法:单例实例和依赖注入。两者各有优点,具体取决于您的用例。为了帮助模拟 Prisma Client,将使用 jest-mock-extended
包。
npm install [email protected] --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: '[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!')
)
})