← Back to Blog

The Ultimate Guide to Testing with Prisma: Mocking Prisma Client

Sabin Adams
Sabin Adams
December 22, 2022
Updated July 9, 2026

To unit test code that uses Prisma ORM, you mock Prisma Client so the function under test runs against a fake client instead of a real database. This article shows how to build that mock with Vitest and the vitest-mock-extended library, then use it to control query responses, trigger errors, mock transactions, and spy on methods. This is part 1 of a five-part series on testing with Prisma ORM.

Updated (July 2026): This article was rewritten for Prisma ORM 7 and the current JavaScript testing stack. Every command and code sample here was executed against Prisma ORM 7.8.0, @prisma/client 7.8.0, @prisma/adapter-pg 7.8.0, Vitest 4.1.10, and vitest-mock-extended 4.0.0 on Node.js 22. The project uses the prisma-client generator, a driver adapter, a prisma.config.ts file for the connection URL, and a local Prisma Postgres database. The mocking approach itself is unchanged; the setup and a few Vitest matcher names have moved on since the original 2022 article.

Introduction

Testing lets developers work confidently and iterate on their products efficiently. So why doesn't everyone write tests? A common answer: writing tests, especially when a database is involved, can be tricky.

Testing meme

Warning: bad advice 👆🏻

In this series, you will learn how to perform different types of tests against applications that interact with a database. This article dives into mocking and walks through how to mock Prisma Client, then looks at what you can do with the mocked client.

Technologies you will use

  • Vitest 4
  • Prisma ORM 7 with the prisma-client generator and the @prisma/adapter-pg driver adapter
  • Prisma Postgres running locally
  • Node.js 20 or later

Assumed knowledge

The following will help you follow along:

  • Basic knowledge of JavaScript or TypeScript
  • Basic knowledge of Prisma Client and its queries
  • Node.js installed

What is a mock?

The first concept in this series is mocking. This term refers to creating a controlled replacement for an object that behaves like the real object it replaces.

The goal of mocking is to replace any external dependencies a function relies on so you can write unit tests against that function in isolation. Tests then focus on the function's own behavior without depending on external modules.

Note: You will look at unit tests in depth in part 2 of this series.

Consider the following function:

import { isValidEmail } from './validators'
import mailer from 'mail-service'

async function sendEmail(to: string, message: string) {
  // 1
  if (!isValidEmail(to)) {
    // 2
    throw new Error('Please provide a valid email address')
  }

  // 3
  mailer.send({ to, message })
}

This function does three things:

  1. Checks that a valid email address is provided
  2. Throws an error if the address is invalid
  3. Sends an email via an imaginary mailer service

To test that this function behaves as expected, you would start by passing an invalid email address and verifying an error is thrown.

The function relies on two external pieces of code: isValidEmail and mailer. Because these are separate and unrelated to the function under test, you do not want your test to depend on them, and you certainly do not want a real email sent when mailer.send() is called.

In situations like this, you mock those dependencies, replacing the real object with a fake that returns a controlled value. This lets you trigger specific states in the target function without considering the behavior of another module.

The rest of this article digs into the patterns and tools you can use to mock modules and use those mocks to test specific scenarios.

Set up a Prisma project

Before writing tests, you need a project. Create a new directory, initialize it, and install the dependencies:

mkdir prisma-testing && cd prisma-testing
npm init -y
npm install -D prisma typescript tsx @types/node vitest vitest-mock-extended
npm install @prisma/client @prisma/adapter-pg dotenv

The prisma-client generator emits modern ES modules, so mark the project as ESM in package.json:

{
  "name": "prisma-testing",
  "type": "module"
}

Create a prisma/schema.prisma file. On Prisma ORM 7, the generator is prisma-client (not the legacy prisma-client-js), and it writes the client to an output path you own. The connection URL no longer lives in the datasource block; only the provider does:

// prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id    Int     @id @default(autoincrement())
  email String? @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

The connection URL is supplied through a prisma.config.ts file at the project root. Prisma 7 does not load .env automatically, so import dotenv/config here:

// prisma.config.ts
import 'dotenv/config'
import { defineConfig } from 'prisma/config'

export default defineConfig({
  schema: 'prisma/schema.prisma',
  datasource: {
    url: process.env.DATABASE_URL!
  }
})

Now start a local Prisma Postgres database. This command runs a local server (powered by PGlite) and prints a connection string:

npx prisma dev -n testing
✔  Your local Prisma Postgres server testing is now running 👍

🔌 To connect with Prisma ORM use the following connection strings:

   DATABASE_URL="postgres://postgres:postgres@localhost:51254/template1?sslmode=disable&connection_limit=10&connect_timeout=0&max_idle_connection_lifetime=0&pool_timeout=0&socket_timeout=0"

Leave that process running. In a second terminal, create a .env file with the DATABASE_URL value it printed:

# .env
DATABASE_URL="postgres://postgres:postgres@localhost:51254/template1?sslmode=disable&connection_limit=10&connect_timeout=0&max_idle_connection_lifetime=0&pool_timeout=0&socket_timeout=0"

Push the schema to the database, then generate the client:

npx prisma db push
npx prisma generate
🚀  Your database is now in sync with your Prisma schema. Done in 71ms

✔ Generated Prisma Client (7.8.0) to ./src/generated/prisma in 43ms

Note: On Prisma 7, prisma db push and prisma migrate dev no longer regenerate the client. After any schema change, run npx prisma generate yourself. This was verified: adding a model and running db push left the generated client unchanged until generate was run.

Set up Vitest

You installed Vitest above. Add a test script to package.json:

{
  "name": "prisma-testing",
  "type": "module",
  "scripts": {
    "test": "vitest"
  }
}

Create a test folder for your tests:

mkdir test

Note: Vitest does not require a test folder. It detects test files by naming convention.

Add a test/sample.test.ts file to confirm Vitest works:

// test/sample.test.ts
import { expect, test } from 'vitest'

test('1 === 1', () => {
  expect(1).toBe(1)
})

Run npm test. The test passes, so Vitest is ready.

Why mock Prisma Client?

The clearest way to show why mocking Prisma Client is useful is to write a function that uses it and test that function without a mock.

Create src/lib/prisma.ts. On Prisma 7 you instantiate the client with a driver adapter, and you import the client from the path the generator wrote to (not from @prisma/client). Because env is read here, import dotenv/config:

// src/lib/prisma.ts
import 'dotenv/config'
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../generated/prisma/client'

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })
const prisma = new PrismaClient({ adapter })

export default prisma

Now write a function that uses that client. Create src/script.ts:

// src/script.ts
import { Prisma } from './generated/prisma/client'
import prisma from './lib/prisma'

// 1
export const createUser = async (user: Prisma.UserCreateInput) => {
  // 2 & 3
  return await prisma.user.create({
    data: user
  })
}

The createUser function:

  1. Takes a user argument
  2. Passes user to prisma.user.create
  3. Returns the response, which is the new user object

Write a test for it that checks createUser returns the created user. Update test/sample.test.ts:

// test/sample.test.ts
import { expect, test } from 'vitest'
import { createUser } from '../src/script'

test('createUser should return the generated user', async () => {
  const newUser = { email: 'user@prisma.io', name: 'Prisma Fan' }
  const user = await createUser(newUser)
  expect(user).toStrictEqual({ ...newUser, id: 1 })
})

Note: This test is not using a mocked Prisma Client. It uses the real client to demonstrate the problem.

If your database has no users yet, this test passes the first time. There are a few problems:

  • The next run creates a user with id 2, so the assertion on id: 1 fails.
  • The email field has a @unique attribute, so a second run throws a unique constraint error.
  • The test requires a running database and writes a record on every run.

For unit testing, which focuses on a single function, the best practice is to assume your database operations behave correctly and use a mocked client instead. That lets you focus on the behavior of the function you are targeting.

Note: There are cases where you want to test against a real database, such as integration and end-to-end tests. Those are covered in part 3 and part 4.

Mock Prisma Client

To properly unit test functions that use Prisma Client, create a mock of the client. This mock replaces the imported module the function normally uses.

You will use Vitest's mocking tools and a library called vitest-mock-extended, which provides a deep-mocking helper. You already installed it above.

Head to test/sample.test.ts and tell Vitest to mock the src/lib/prisma.ts module:

// test/sample.test.ts
import { expect, test, vi } from 'vitest' // 👈🏻 added the `vi` import
import { createUser } from '../src/script'

vi.mock('../src/lib/prisma')

test('createUser should return the generated user', async () => {
  const newUser = { email: 'user@prisma.io', name: 'Prisma Fan' }
  const user = await createUser(newUser)
  expect(user).toStrictEqual({ ...newUser, id: 1 })
})

The vi.mock function tells Vitest to mock the module at the given path. There are several ways it decides how to mock a module, described in the documentation.

By default, Vitest cannot automatically mock the deeply nested properties of the prisma object. For example, prisma.user.create() will not be mocked correctly because it is a nested property. To solve this, provide a manual mock that returns a deep mock of the client.

Create a __mocks__ folder next to src/lib/prisma.ts:

mkdir src/lib/__mocks__

The __mocks__ folder is a common testing convention for manually created mocks. It must sit directly adjacent to the module it mocks, which is why it goes next to src/lib/prisma.ts.

Inside it, create a file with the same name as the real module, prisma.ts:

// src/lib/__mocks__/prisma.ts
// 1
import { beforeEach } from 'vitest'
import { mockDeep, mockReset } from 'vitest-mock-extended'
import type { PrismaClient } from '../../generated/prisma/client'

// 2
beforeEach(() => {
  mockReset(prisma)
})

// 3
const prisma = mockDeep<PrismaClient>()
export default prisma

The snippet above:

  1. Imports the tools needed to create the mocked client, using a type-only import of PrismaClient from the generated path.
  2. Resets the mock to its original state between each test.
  3. Creates and exports a deep mock of Prisma Client with mockDeep, which mocks all properties, including deeply nested ones.

Note: mockDeep sets every Prisma Client function to the Vitest helper vi.fn().

Because the mock is now in place, prisma.user.create no longer hits the database. Right now it returns undefined, so the test fails. Tell Vitest what prisma.user.create should return by mocking its behavior in the test:

// test/sample.test.ts
import { expect, test, vi } from 'vitest'
import { createUser } from '../src/script'
import prisma from '../src/lib/__mocks__/prisma' // 👈🏻 import the mocked client

vi.mock('../src/lib/prisma')

test('createUser should return the generated user', async () => {
  const newUser = { email: 'user@prisma.io', name: 'Prisma Fan' }
  prisma.user.create.mockResolvedValue({ ...newUser, id: 1 }) // 👈🏻 mock the response
  const user = await createUser(newUser)
  expect(user).toStrictEqual({ ...newUser, id: 1 })
})

mockResolvedValue replaces prisma.user.create with a function that resolves to the value you provide. For the duration of that test, the call behaves as if you wrote:

prisma.user.create = async () => ({ ...newUser, id: 1 })

Now you can run functions that use Prisma Client by mocking the client's behavior beforehand. Rather than worrying about individual queries, you focus on the function's business logic.

Running the tests now, they pass:

 ✓ test/sample.test.ts (1 test)

 Test Files  1 passed (1)
      Tests  1 passed (1)

Using the mocked client

You have a mocked Prisma Client and the ability to control query results. The rest of this article covers the functions your mocked client and Vitest offer.

Note: The examples below are functional samples of the tools available, not full unit tests. Part 2 covers unit testing in depth.

Mocking query responses

One of the most common uses of the mocked client is mocking query responses. You already mocked create; there are multiple ways to do this, each with its own use case.

Take this scenario:

// src/script.ts
export const getPosts = async () => {
  const published = await prisma.post.findMany({ where: { published: true } })
  const unpublished = await prisma.post.findMany({ where: { published: false } })

  return { published, unpublished }
}

A single mockResolvedValue would return the same array for both findMany calls, which is unrealistic. Use mockResolvedValueOnce, which can be chained to mock the first and second responses independently:

// test/sample.test.ts
import { getPosts } from '../src/script'

test('getPosts should separate published & un-published posts', async () => {
  const mockPublishedPost = {
    id: 1,
    content: 'content',
    published: true,
    title: 'title',
    authorId: 1
  }

  prisma.post.findMany
    .mockResolvedValueOnce([mockPublishedPost])
    .mockResolvedValueOnce([{ ...mockPublishedPost, published: false }])

  const posts = await getPosts()
  expect(posts).toStrictEqual({
    published: [mockPublishedPost],
    unpublished: [{ ...mockPublishedPost, published: false }]
  })
})

Note: toStrictEqual checks both structure and type when comparing objects.

Triggering and capturing errors

You may want to test a case where a query throws. A good example is Prisma Client's findUniqueOrThrow, which throws if no record is found. Because the client is mocked, you trigger the errored state yourself:

// src/script.ts
export const getPostByID = async (id: number) => {
  try {
    return await prisma.post.findUniqueOrThrow({ where: { id } })
  } catch (e: any) {
    return e.message
  }
}
// test/sample.test.ts
import { getPostByID } from '../src/script'

test('getPostByID returns the error message when no post is found', async () => {
  prisma.post.findUniqueOrThrow.mockImplementation((() => {
    throw new Error('There was an error.')
  }) as any)

  const response = await getPostByID(200)

  expect(response).toBe('There was an error.')
})

mockImplementation replaces the mocked function's behavior. Here it throws an error, giving you fine-grained control over the output in error states.

If the function under test is meant to throw rather than catch, you can test that too:

// src/script.ts (throwing variant)
export const getPostByID = async (id: number) => {
  return await prisma.post.findUniqueOrThrow({ where: { id } })
}
// test/errors.test.ts
test('getPostByID should throw an error', async () => {
  prisma.post.findUniqueOrThrow.mockImplementation((() => {
    throw new Error('There was an error.')
  }) as any)

  await expect(getPostByID(1)).rejects.toThrow()
  await expect(getPostByID(1)).rejects.toThrowError('There was an error')
})

The rejects modifier resolves the promise given to expect and checks for a rejection. toThrow and toThrowError then check details about the error.

Mocking transactions

Another part of Prisma Client you may need to mock is a $transaction. There are two forms: sequential operations and interactive transactions. How you mock them depends on your test's goal.

If your test only cares about the result of a sequential transaction, mock the return value of $transaction directly:

// src/script-tx.ts
export const addPost = async (data: Prisma.PostCreateInput) => {
  const [newPost, count] = await prisma.$transaction([
    prisma.post.create({ data }),
    prisma.post.count()
  ])

  return { newPost, count }
}
// test/transactions.test.ts
test('addPost (sequential) returns the new post and the total count', async () => {
  // 1
  const mockPost = {
    authorId: 1,
    title: 'title',
    content: 'content',
    published: true
  }

  // 2
  const mockResponse = [{ ...mockPost, id: 1 }, 100]
  prisma.$transaction.mockResolvedValue(mockResponse)

  // 3
  const data = await addPost(mockPost)

  // 4
  expect(data).toStrictEqual({
    newPost: mockResponse[0],
    count: mockResponse[1]
  })
})

In the test above you:

  1. Mocked the data for the post you intend to create.
  2. Mocked what $transaction should return.
  3. Invoked the function after the mocks were set up.
  4. Verified the returned value matched what you expected.

By mocking the response of $transaction itself, you did not have to consider what happened inside the transaction.

To test an interactive transaction with business logic you need to validate, mock the individual calls and the $transaction implementation:

// src/script-itx.ts
export const addPost = async (data: Prisma.PostCreateInput) => {
  return await prisma.$transaction(async (tx) => {
    if (!('published' in data)) {
      data['published'] = true
    }

    const newPost = await tx.post.create({ data })
    const count = await tx.post.count()

    return { newPost, count }
  })
}
// test/transactions.test.ts
test('addPost (interactive) returns the new post and the total count', async () => {
  // 1
  const mockPost = { authorId: 1, title: 'title', content: 'content' }
  const mockResponse = {
    newPost: { ...mockPost, id: 1, published: true },
    count: 100
  }

  // 2
  prisma.post.create.mockResolvedValue(mockResponse.newPost)
  prisma.post.count.mockResolvedValue(mockResponse.count)

  // 3
  prisma.$transaction.mockImplementation((callback) => callback(prisma))

  // 4
  const data = await addPost(mockPost)

  // 5
  expect(data.newPost.published).toBe(true)
  expect(data).toStrictEqual(mockResponse)
})

Here you:

  1. Mock the post and response objects.
  2. Mock the responses of create and count.
  3. Mock the $transaction implementation so it passes the mocked client to the interactive callback rather than a real client.
  4. Invoke addPost.
  5. Validate the response, confirming the business logic set published to true.

Spy on methods

The last concept is spying. Vitest lets you spy on a function to observe how many times it was called, what arguments it received, and what it returned, without changing its behavior.

A mocked function created with vi.fn() has all spying features by default. Because Prisma Client is mocked, every function can be spied on:

// src/script.ts
export const updateUser = async (
  id: number,
  data: Prisma.UserUpdateInput,
  clearPosts: boolean
) => {
  const user = await prisma.user.update({ where: { id }, data })
  if (clearPosts) {
    await prisma.post.deleteMany({ where: { authorId: id } })
  }

  return user
}
// test/sample.test.ts
import { updateUser } from '../src/script'

test('updateUser should delete user posts if clearPosts is true', async () => {
  prisma.user.update.mockResolvedValue({
    id: 1,
    email: 'adams@prisma.io',
    name: 'Sabin Adams'
  })

  await updateUser(1, {}, true)

  expect(prisma.post.deleteMany).toHaveBeenCalled()
  expect(prisma.post.deleteMany).toHaveBeenCalledWith({
    where: { authorId: 1 }
  })
})

Spy functions are useful when you want to confirm certain code paths run based on the inputs.

Running the full set of mock and spy tests, they all pass:

 Test Files  3 passed (3)
      Tests  7 passed (7)

Why Vitest?

This series uses Vitest rather than Jest for a few practical reasons. Vitest runs your tests through the same Vite pipeline your app already uses, so ESM and TypeScript work without extra configuration, which matters because the Prisma 7 prisma-client generator emits ES modules. Vitest's API mirrors Jest's, so the concepts here transfer if your team uses Jest elsewhere. Jest remains a capable choice; the guidance in this series applies to either with minor changes to configuration and imports.

Frequently asked questions

Summary & what's next

In this article, you focused on mocking and spying, which play a major role in unit testing. Specifically, you explored:

  • What mocking is and why it is useful
  • How to set up a project with Vitest and Prisma ORM 7 configured
  • How to mock Prisma Client with a deep mock
  • How to use a mocked Prisma Client instance to control responses, trigger errors, mock transactions, and spy on methods

With this foundation, you have the tools to unit test an application. In part 2: Unit Testing, you will put them to use against a real service.

Looking ahead: Prisma Next is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents and currently in early access. It becomes Prisma 8 at general availability; until then, Prisma 7 stays the production choice. To try it, run npm create prisma@next or read the early access docs.

Build your next app with Prisma

Start free. Scale when you’re ready.

Try Prisma
Share this article