Skip to content
On this page

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 应用程序的数据层稳定可靠,并及早发现潜在问题。