跳至主要内容

单元测试

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

  • 快速查找并隔离代码中的错误。
  • 通过指示某些代码块应该做什么来为每个代码模块提供文档。
  • 一个有用的指标,表明重构进展顺利。代码重构后,测试应该仍然通过。

在 Prisma ORM 的上下文中,这通常意味着测试使用 Prisma 客户端进行数据库调用的函数。

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

这意味着您应该尽可能减少依赖项,例如外部服务和数据库,以使测试及其环境尽可能轻量级。

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

先决条件

本指南假定您已经在您的项目中设置了 JavaScript 测试库Jestts-jest

模拟 Prisma 客户端

为了确保您的单元测试与外部因素隔离,您可以模拟 Prisma 客户端,这意味着您可以享受使用模式(类型安全)的好处,而无需在运行测试时实际调用数据库。

本指南将介绍两种模拟 Prisma 客户端的方法:单例实例和依赖项注入。两种方法都有其优点,具体取决于您的用例。为了帮助模拟 Prisma 客户端,我们将使用jest-mock-extended包。

npm install [email protected] --save-dev
危险

在撰写本文时,本指南使用的是 jest-mock-extended 版本 ^2.0.4

单例

以下步骤指导您使用单例模式模拟 Prisma 客户端。

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

    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 客户端实例),并使用 jest-mock-extended 中的 mockDeep 方法来启用对 Prisma 客户端上可用对象和方法的访问。然后,它在每个测试运行之前重置模拟的实例。

接下来,将 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 客户端看到循环依赖错误,请尝试将 "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 客户端进行模拟调用并运行要测试的查询。ctx 上下文将用于运行要针对其进行测试的场景查询。

示例单元测试

使用 Prisma ORM 进行单元测试的一个实际用例可能是注册表单。您的用户填写一个表单,该表单会调用一个函数,该函数会使用 Prisma 客户端对您的数据库进行调用。

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

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

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

  • 创建新用户
  • 更新用户的姓名
  • 如果未接受条款,则创建用户失败

使用依赖项注入模式的函数将被注入上下文(作为参数传递)到它们,而使用单例模式的函数将使用 Prisma 客户端的单例实例。

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 客户端的使用方式。

依赖项注入示例将上下文传递到要测试的函数中,并使用它来调用模拟实现。

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

__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: '[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!')
)
})
__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: '[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!')
)
})