Appearance
Prisma 测试
为 Prisma 应用程序编写测试是确保数据层可靠性的重要环节。本指南将详细介绍如何为 Prisma 项目编写单元测试、集成测试和端到端测试。
测试环境配置
安装测试依赖
bash
# 使用 Jest
npm install -D jest @types/jest ts-jest
# 或使用 Vitest (更快的测试运行器)
npm install -D vitest @vitest/ui jsdom happy-dom
# Prisma 测试工具
npm install -D prisma @prisma/client
# 测试数据库工具
npm install -D @shelf/jest-mongodb dotenv
测试配置文件
javascript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
}
或者 Vitest 配置:
javascript
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'node',
setupFiles: ['./tests/setup.ts'],
include: ['tests/**/*.test.ts'],
},
})
测试数据库设置
使用独立的测试数据库
typescript
// tests/setup.ts
import { execSync } from 'child_process'
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
let prisma: PrismaClient
beforeAll(async () => {
// 为测试创建独立的数据库
const dbName = `test_${Date.now()}`
process.env.DATABASE_URL = `postgresql://user:password@localhost:5432/${dbName}`
// 创建测试数据库
execSync(`createdb ${dbName}`)
// 应用迁移
execSync('npx prisma migrate deploy')
// 初始化 Prisma 客户端
prisma = new PrismaClient()
global.prisma = prisma
})
beforeEach(async () => {
// 在每个测试前清空数据库
await prisma.$transaction([
prisma.comment.deleteMany({}),
prisma.post.deleteMany({}),
prisma.user.deleteMany({}),
prisma.profile.deleteMany({}),
])
})
afterAll(async () => {
// 清理测试数据库
await prisma.$disconnect()
const dbName = process.env.DATABASE_URL?.split('/').pop()
execSync(`dropdb ${dbName}`)
})
使用内存数据库进行测试
typescript
// tests/test-helpers/memory-db.ts
import { PrismaClient } from '@prisma/client'
import { execSync } from 'child_process'
// 使用 SQLite 内存数据库进行快速测试
export function createTestDatabase() {
// 创建临时 SQLite 数据库
const dbPath = `file:./test_${Date.now()}.db`
process.env.DATABASE_URL = dbPath
// 应用迁移到内存数据库
execSync('npx prisma migrate reset --force')
execSync('npx prisma db push')
return new PrismaClient()
}
// 在测试中使用
let testPrisma: PrismaClient
beforeAll(() => {
testPrisma = createTestDatabase()
})
afterAll(async () => {
await testPrisma.$disconnect()
})
基础 CRUD 测试
用户模型测试
typescript
// tests/models/user.test.ts
import { PrismaClient, User } from '@prisma/client'
let prisma: PrismaClient
beforeAll(() => {
prisma = new PrismaClient()
})
afterAll(async () => {
await prisma.$disconnect()
})
describe('User Model', () => {
beforeEach(async () => {
// 清空用户表
await prisma.user.deleteMany({})
})
describe('Create', () => {
it('should create a new user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
}
const user = await prisma.user.create({
data: userData,
})
expect(user).toHaveProperty('id')
expect(user.email).toBe(userData.email)
expect(user.name).toBe(userData.name)
expect(user.createdAt).toBeInstanceOf(Date)
})
it('should throw error for duplicate email', async () => {
const userData = {
email: 'duplicate@example.com',
name: 'User 1',
}
await prisma.user.create({ data: userData })
await expect(
prisma.user.create({
data: { ...userData, name: 'User 2' },
})
).rejects.toThrow()
})
})
describe('Read', () => {
let testUser: User
beforeEach(async () => {
testUser = await prisma.user.create({
data: {
email: 'read-test@example.com',
name: 'Read Test',
},
})
})
it('should find user by id', async () => {
const foundUser = await prisma.user.findUnique({
where: { id: testUser.id },
})
expect(foundUser).not.toBeNull()
expect(foundUser?.id).toBe(testUser.id)
expect(foundUser?.email).toBe(testUser.email)
})
it('should find user by email', async () => {
const foundUser = await prisma.user.findUnique({
where: { email: testUser.email },
})
expect(foundUser).not.toBeNull()
expect(foundUser?.id).toBe(testUser.id)
})
it('should return null for non-existent user', async () => {
const user = await prisma.user.findUnique({
where: { id: 999999 },
})
expect(user).toBeNull()
})
})
describe('Update', () => {
let testUser: User
beforeEach(async () => {
testUser = await prisma.user.create({
data: {
email: 'update-test@example.com',
name: 'Original Name',
},
})
})
it('should update user name', async () => {
const updatedUser = await prisma.user.update({
where: { id: testUser.id },
data: { name: 'Updated Name' },
})
expect(updatedUser.name).toBe('Updated Name')
expect(updatedUser.updatedAt.getTime()).toBeGreaterThan(
updatedUser.createdAt.getTime()
)
})
it('should throw error for updating non-existent user', async () => {
await expect(
prisma.user.update({
where: { id: 999999 },
data: { name: 'New Name' },
})
).rejects.toThrow()
})
})
describe('Delete', () => {
let testUser: User
beforeEach(async () => {
testUser = await prisma.user.create({
data: {
email: 'delete-test@example.com',
name: 'Delete Test',
},
})
})
it('should delete user', async () => {
const deletedUser = await prisma.user.delete({
where: { id: testUser.id },
})
expect(deletedUser.id).toBe(testUser.id)
// Verify user is actually deleted
const foundUser = await prisma.user.findUnique({
where: { id: testUser.id },
})
expect(foundUser).toBeNull()
})
})
})
关系测试
测试关联数据操作
typescript
// tests/relations.test.ts
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
beforeAll(() => {
prisma = new PrismaClient()
})
afterAll(async () => {
await prisma.$disconnect()
})
describe('Relationship Tests', () => {
beforeEach(async () => {
await prisma.$transaction([
prisma.comment.deleteMany({}),
prisma.post.deleteMany({}),
prisma.user.deleteMany({}),
])
})
it('should create user with posts', async () => {
const userWithPosts = await prisma.user.create({
data: {
email: 'author@example.com',
name: 'Author Name',
posts: {
create: [
{
title: 'First Post',
content: 'Content of first post',
},
{
title: 'Second Post',
content: 'Content of second post',
},
],
},
},
include: {
posts: true,
},
})
expect(userWithPosts.posts).toHaveLength(2)
expect(userWithPosts.posts[0].authorId).toBe(userWithPosts.id)
})
it('should query users with published posts', async () => {
// 创建测试数据
const user = await prisma.user.create({
data: {
email: 'publisher@example.com',
name: 'Publisher',
posts: {
create: [
{
title: 'Published Post',
content: 'Published content',
published: true,
},
{
title: 'Draft Post',
content: 'Draft content',
published: false,
},
],
},
},
})
// 查询有已发布文章的用户
const usersWithPublishedPosts = await prisma.user.findMany({
where: {
posts: {
some: {
published: true,
},
},
},
include: {
posts: {
where: {
published: true,
},
},
},
})
expect(usersWithPublishedPosts).toHaveLength(1)
expect(usersWithPublishedPosts[0].posts).toHaveLength(1)
expect(usersWithPublishedPosts[0].posts[0].published).toBe(true)
})
it('should handle nested updates', async () => {
const result = await prisma.user.create({
data: {
email: 'nested-update@example.com',
name: 'Nested Update User',
posts: {
create: [
{
title: 'Original Post',
content: 'Original content',
},
],
},
},
include: {
posts: true,
},
})
// 更新用户信息和文章信息
const updatedResult = await prisma.user.update({
where: { id: result.id },
data: {
name: 'Updated User Name',
posts: {
update: {
where: { id: result.posts[0].id },
data: {
title: 'Updated Post Title',
content: 'Updated content',
},
},
},
},
include: {
posts: true,
},
})
expect(updatedResult.name).toBe('Updated User Name')
expect(updatedResult.posts[0].title).toBe('Updated Post Title')
})
})
事务测试
事务回滚测试
typescript
// tests/transactions.test.ts
import { Prisma } from '@prisma/client'
describe('Transaction Tests', () => {
it('should rollback transaction on error', async () => {
const initialUserCount = await prisma.user.count()
try {
await prisma.$transaction(async (tx) => {
// 创建用户
await tx.user.create({
data: {
email: 'transaction-test@example.com',
name: 'Transaction Test',
},
})
// 创建相关帖子
await tx.post.create({
data: {
title: 'Transaction Post',
content: 'Transaction content',
authorId: 999999, // 故意使用不存在的用户ID以触发错误
},
})
})
} catch (error) {
// 验证事务已回滚,用户未被创建
const finalUserCount = await prisma.user.count()
expect(finalUserCount).toBe(initialUserCount)
expect(error).toBeDefined()
}
})
it('should commit successful transaction', async () => {
const initialUserCount = await prisma.user.count()
const result = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: 'successful-transaction@example.com',
name: 'Successful Transaction',
},
})
const post = await tx.post.create({
data: {
title: 'Successful Post',
content: 'Successful content',
authorId: user.id,
},
})
return { user, post }
})
// 验证事务已提交
const finalUserCount = await prisma.user.count()
expect(finalUserCount).toBe(initialUserCount + 1)
expect(result.user.email).toBe('successful-transaction@example.com')
expect(result.post.title).toBe('Successful Post')
})
})
服务层测试
测试业务逻辑
typescript
// src/services/user-service.ts
import { PrismaClient, User } from '@prisma/client'
interface CreateUserInput {
email: string
name?: string
}
interface UpdateUserInput {
name?: string
email?: string
}
export class UserService {
constructor(private prisma: PrismaClient) {}
async createUser(input: CreateUserInput): Promise<User> {
return await this.prisma.user.create({
data: {
email: input.email,
name: input.name,
},
})
}
async getUserWithPosts(userId: number) {
return await this.prisma.user.findUnique({
where: { id: userId },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
},
},
})
}
async updateUser(id: number, input: UpdateUserInput) {
return await this.prisma.user.update({
where: { id },
data: input,
})
}
}
// tests/services/user-service.test.ts
import { PrismaClient } from '@prisma/client'
import { UserService } from '../../src/services/user-service'
let prisma: PrismaClient
let userService: UserService
beforeAll(() => {
prisma = new PrismaClient()
userService = new UserService(prisma)
})
afterAll(async () => {
await prisma.$disconnect()
})
describe('UserService', () => {
beforeEach(async () => {
await prisma.user.deleteMany({})
await prisma.post.deleteMany({})
})
describe('createUser', () => {
it('should create a new user', async () => {
const userData = {
email: 'service-test@example.com',
name: 'Service Test User',
}
const user = await userService.createUser(userData)
expect(user.email).toBe(userData.email)
expect(user.name).toBe(userData.name)
})
})
describe('getUserWithPosts', () => {
it('should return user with published posts', async () => {
const user = await prisma.user.create({
data: {
email: 'with-posts@example.com',
name: 'User With Posts',
posts: {
create: [
{ title: 'Published Post', published: true },
{ title: 'Draft Post', published: false },
],
},
},
})
const result = await userService.getUserWithPosts(user.id)
expect(result).not.toBeNull()
expect(result?.posts).toHaveLength(1) // 只返回已发布的文章
expect(result?.posts[0].published).toBe(true)
})
})
})
测试工具和实用程序
测试数据工厂
typescript
// tests/factories/user-factory.ts
import { PrismaClient, Prisma } from '@prisma/client'
export class UserFactory {
constructor(private prisma: PrismaClient) {}
async create(attributes: Partial<Prisma.UserCreateInput> = {}) {
const userData: Prisma.UserCreateInput = {
email: `test${Date.now()}${Math.random()}@example.com`,
name: 'Test User',
...attributes,
}
return await this.prisma.user.create({
data: userData,
})
}
async createMany(count: number, attributes: Partial<Prisma.UserCreateInput> = {}) {
const users = []
for (let i = 0; i < count; i++) {
users.push(await this.create(attributes))
}
return users
}
}
// tests/factories/post-factory.ts
export class PostFactory {
constructor(private prisma: PrismaClient) {}
async create(attributes: Partial<Prisma.PostCreateInput> = {}) {
// 如果没有提供作者,创建一个测试用户
if (!attributes.author) {
const user = await new UserFactory(this.prisma).create()
attributes.author = { connect: { id: user.id } }
}
const postData: Prisma.PostCreateInput = {
title: 'Test Post',
content: 'Test content',
published: false,
...attributes,
}
return await this.prisma.post.create({
data: postData,
})
}
}
// 使用工厂的测试
describe('Using Factories', () => {
it('should create user with factory', async () => {
const userFactory = new UserFactory(prisma)
const user = await userFactory.create({
name: 'Factory Created User',
})
expect(user.name).toBe('Factory Created User')
expect(user.email).toMatch(/.+@example\.com/)
})
})
性能测试
查询性能测试
typescript
// tests/performance.test.ts
import { PrismaClient } from '@prisma/client'
describe('Performance Tests', () => {
let prisma: PrismaClient
beforeAll(() => {
prisma = new PrismaClient()
})
afterAll(async () => {
await prisma.$disconnect()
})
it('should query efficiently with proper indexing', async () => {
// 创建大量测试数据
const users = await Promise.all(
Array.from({ length: 100 }, (_, i) =>
prisma.user.create({
data: {
email: `perf${i}@example.com`,
name: `Perf User ${i}`,
},
})
)
)
const startTime = performance.now()
// 执行查询
const result = await prisma.user.findMany({
where: {
email: {
contains: 'perf',
},
},
take: 50,
orderBy: {
id: 'asc',
},
})
const endTime = performance.now()
const duration = endTime - startTime
// 验证查询时间和结果
expect(result).toHaveLength(50)
expect(duration).toBeLessThan(1000) // 查询应在1秒内完成
}, 10000) // 增加测试超时时间
})
测试最佳实践
测试组织和结构
typescript
// 测试文件组织示例
// tests/
// ├── setup.ts # 测试设置
// ├── teardown.ts # 测试清理
// ├── factories/ # 测试数据工厂
// │ ├── user-factory.ts
// │ └── post-factory.ts
// ├── models/ # 模型测试
// │ ├── user.test.ts
// │ └── post.test.ts
// ├── services/ # 服务层测试
// │ └── user-service.test.ts
// ├── integration/ # 集成测试
// │ └── auth-flow.test.ts
// └── performance/ # 性能测试
// └── queries.test.ts
通过实施这些测试策略,您可以确保 Prisma 应用程序的数据层稳定可靠,并及早发现潜在问题。