跳到主要内容

单元测试

单元测试旨在隔离一小部分(单元)代码并测试其逻辑上可预测的行为。它通常涉及模拟对象或服务器响应以模拟真实世界的行为。单元测试的一些好处包括:

  • 快速发现和隔离代码中的错误。
  • 通过指示特定代码块应执行的操作,为每个代码模块提供文档。
  • 衡量重构是否顺利的有用指标。代码重构后,测试仍应通过。

在 Prisma ORM 的语境中,这通常意味着测试使用 Prisma Client 进行数据库调用的函数。

单个测试应侧重于您的函数逻辑如何处理不同的输入(例如空值或空列表)。

这意味着您应尽可能移除依赖项,例如外部服务和数据库,以保持测试及其环境尽可能轻量。

注意:这篇博客文章提供了一份在 Express 项目中使用 Prisma ORM 实现单元测试的全面指南。如果您想深入探讨此主题,请务必阅读!

先决条件

本指南假定您已在项目中设置了 JavaScript 测试库 Jestts-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。

  1. 在您的项目根目录创建一个名为 client.ts 的文件,并添加以下代码。这将实例化一个 Prisma Client 实例。

    client.ts
    import { PrismaClient } from '@prisma/client'

    const prisma = new PrismaClient()
    export default prisma
  2. 接下来,在您的项目根目录创建一个名为 singleton.ts 的文件,并添加以下内容

    singleton.ts
    import { 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-extendedmockDeep 方法来访问 Prisma Client 上可用的对象和方法。然后在每次测试运行前重置模拟实例。

接下来,将 setupFilesAfterEnv 属性添加到您的 jest.config.js 文件中,并指定您的 singleton.ts 文件的路径。

jest.config.js
module.exports = {
clearMocks: true,
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/singleton.ts'],
}

依赖注入

另一种流行的模式是依赖注入。

  1. 创建一个 context.ts 文件并添加以下内容

    context.ts
    import { 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 文件中。

  1. 要在测试文件中使用上下文,您可以这样做:

    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 对您的数据库进行调用。

所有以下示例都使用以下模式模型

schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
acceptTermsAndConditions Boolean
}

以下单元测试将模拟以下过程:

  • 创建一个新用户
  • 更新用户名称
  • 如果条款未被接受,则创建用户失败

使用依赖注入模式的函数将注入上下文(作为参数传入),而使用单例模式的函数将使用 Prisma Client 的单例实例。

functions-with-context.ts
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,
})
}
functions-without-context.ts
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。

依赖注入 示例将上下文传递给正在测试的函数,并使用它调用模拟实现。

单例 示例使用单例客户端实例来调用模拟实现。

__tests__/with-singleton.ts
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!')
)
})
__tests__/with-dependency-injection.ts
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!')
)
})
© . All rights reserved.