Skip to main content

单元测试

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

¥Unit testing aims to isolate a small portion (unit) of code and test it for logically predictable behaviors. It generally involves mocking objects or server responses to simulate real world behaviors. Some benefits to unit testing include:

  • 快速查找并隔离代码中的错误。

    ¥Quickly find and isolate bugs in code.

  • 通过指示某些代码块应该做什么来为每个代码模块提供文档。

    ¥Provides documentation for each module of code by way of indicating what certain code blocks should be doing.

  • 一个有用的衡量标准,表明重构进展顺利。代码重构后测试仍应通过。

    ¥A helpful gauge that a refactor has gone well. The tests should still pass after code has been refactored.

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

¥In the context of Prisma ORM, this generally means testing a function which makes database calls using Prisma Client.

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

¥A single test should focus on how your function logic handles different inputs (such as a null value or an empty list).

这意味着你应该致力于删除尽可能多的依赖,例如外部服务和数据库,以保持测试及其环境尽可能轻量级。

¥This means that you should aim to remove as many dependencies as possible, such as external services and databases, to keep the tests and their environments as lightweight as possible.

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

¥Note: This blog post provides a comprehensive guide to implementing unit testing in your Express project with Prisma ORM. If you're looking to delve into this topic, be sure to give it a read!

先决条件

¥Prerequisites

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

¥This guide assumes you have the JavaScript testing library Jest and ts-jest already setup in your project.

模拟 Prisma 客户端

¥Mocking Prisma Client

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

¥To ensure your unit tests are isolated from external factors you can mock Prisma Client, this means you get the benefits of being able to use your schema (type-safety), without having to make actual calls to your database when your tests are run.

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

¥This guide will cover two approaches to mocking Prisma Client, a singleton instance and dependency injection. Both have their merits depending on your use cases. To help with mocking Prisma Client the jest-mock-extended package will be used.

npm install jest-mock-extended@2.0.4 --save-dev
danger

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

¥At the time of writing, this guide uses jest-mock-extended version ^2.0.4.

辛格尔顿

¥Singleton

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

¥The following steps guide you through mocking Prisma Client using a singleton pattern.

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

    ¥Create a file at your projects root called client.ts and add the following code. This will instantiate a Prisma Client instance.

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

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

    ¥Next create a file named singleton.ts at your projects root and add the following:

    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-extended 中的 mockDeep 方法来访问 Prisma Client 上可用的对象和方法。然后,它会在每次测试运行之前重置模拟实例。

¥The singleton file tells Jest to mock a default export (the Prisma Client instance in ./client.ts), and uses the mockDeep method from jest-mock-extended to enable access to the objects and methods available on Prisma Client. It then resets the mocked instance before each test is run.

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

¥Next, add the setupFilesAfterEnv property to your jest.config.js file with the path to your singleton.ts file.

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

依赖注入

¥Dependency injection

另一种可以使用的流行模式是依赖注入。

¥Another popular pattern that can be used is dependency injection.

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

    ¥Create a context.ts file and add the following:

    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

¥If you find that you're seeing a circular dependency error highlighted through mocking Prisma Client, try adding "strictNullChecks": true to your tsconfig.json.

  1. 要使用上下文,你需要在测试文件中执行以下操作:

    ¥To use the context, you would do the following in your test file:

    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 上下文将用于运行测试的场景查询。

¥This will create a new context before each test is run via the createMockContext function. This (mockCtx) context will be used to make a mock call to Prisma Client and run a query to test. The ctx context will be used to run a scenario query that is tested against.

单元测试示例

¥Example unit tests

单元测试 Prisma ORM 的真实用例可能是注册表单。你的用户填写一个调用函数的表单,该函数又使用 Prisma 客户端调用你的数据库。

¥A real world use case for unit testing Prisma ORM might be a signup form. Your user fills in a form which calls a function, which in turn uses Prisma Client to make a call to your database.

以下所有示例均使用以下架构模型:

¥All of the examples that follow use the following schema model:

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

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

¥The following unit tests will mock the process of

  • 创建新用户

    ¥Creating a new user

  • 更新用户名

    ¥Updating a users name

  • 如果不接受条款则无法创建用户

    ¥Failing to create a user if terms are not accepted

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

¥The functions that use the dependency injection pattern will have the context injected (passed in as a parameter) into them, whereas the functions that use the singleton pattern will use the singleton instance of 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 客户端。

¥The tests for each methodology are fairly similar, the difference is how the mocked Prisma Client is used.

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

¥The dependency injection example passes the context through to the function that is being tested as well as using it to call the mock implementation.

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

¥The singleton example uses the singleton client instance to call the mock implementation.

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