https://github.com/quramy/jest-prisma
Jest environment for integrated testing with Prisma client
https://github.com/quramy/jest-prisma
integration-testing jest prisma
Last synced: 6 months ago
JSON representation
Jest environment for integrated testing with Prisma client
- Host: GitHub
- URL: https://github.com/quramy/jest-prisma
- Owner: Quramy
- License: mit
- Created: 2022-06-21T04:40:19.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2025-04-29T17:12:04.000Z (6 months ago)
- Last Synced: 2025-05-14T14:48:05.893Z (6 months ago)
- Topics: integration-testing, jest, prisma
- Language: TypeScript
- Homepage:
- Size: 660 KB
- Stars: 293
- Watchers: 5
- Forks: 17
- Open Issues: 22
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# jest-prisma
[](https://github.com/Quramy/jest-prisma/actions)
[](https://badge.fury.io/js/@quramy%2Fjest-prisma)
[](https://raw.githubusercontent.com/Quramy/jest-prisma/main/LICENSE.txt)
Jest environment for Prisma integrated testing.
You can run each test case in isolated transaction which is rolled back automatically.
## How to use
#### Install
```sh
$ npm i @quramy/jest-prisma -D
```
#### Configure Jest
```js
/* jest.config.mjs */
export default {
// ... Your jest configuration
testEnvironment: "@quramy/jest-prisma/environment",
};
```
#### Configure TypeScript
```js
/* tsconfig.json */
{
"compilerOptions": {
"types": ["@types/jest", "@quramy/jest-prisma"],
}
}
```
#### Configure Prisma
jest-prisma uses [Prisma interactive transaction feature](https://www.prisma.io/docs/guides/performance-and-optimization/prisma-client-transactions-guide#interactive-transactions). Interactive transaction needs to be listed in `previewFeatures` if you use `@prisma/client` < 4.7 .
#### Write tests
Global object `jestPrisma` is provided within jest-prisma environment. And Prisma client instance is available via `jestPrisma.client`
```ts
describe(UserService, () => {
// jestPrisma.client works with transaction rolled-back automatically after each test case end.
const prisma = jestPrisma.client;
test("Add user", async () => {
const createdUser = await prisma.user.create({
data: {
id: "001",
name: "quramy",
},
});
expect(
await prisma.user.findFirst({
where: {
name: "quramy",
},
}),
).toStrictEqual(createdUser);
});
test("Count user", async () => {
expect(await prisma.user.count()).toBe(0);
});
});
```
## Configuration
You can pass some options using `testEnvironmentOptions`.
```js
/* jest.config.mjs */
export default {
testEnvironment: "@quramy/jest-prisma/environment",
testEnvironmentOptions: {
verboseQuery: true,
},
};
```
Alternatively, you can use `@jest-environment-options` pragma in your test file:
```js
/**
*
* @jest-environment-options: { "verboseQuery": true }
*
*/
test("it should execute prisma client", () => {
/* .... */
});
```
## Use customized `PrismaClient` instance
By default, jest-prisma instantiates and uses Prisma client instance from `@prisma/client`.
Sometimes you want to use customized (or extended) Prisma client instance, such as:
```ts
/* src/client.ts */
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient().$extends({
client: {
$myMethod: () => {
/* ... */
},
},
});
```
You need configure jest-prisma by the following steps.
First, declare type of `global.jestPrisma` variable:
```ts
/* typeDefs/jest-prisma.d.ts */
import type { JestPrisma } from "@quramy/jest-prisma-core";
import type { prisma } from "../src/client";
declare global {
var jestPrisma: JestPrisma;
}
```
And add the path of this declaration to your tsconfig.json:
```js
/* tsconfig.json */
{
"compilerOptions": {
"types": ["@types/jest"], // You don't need list "@quramy/jest-prisma"
},
"includes": ["typeDefs/jest-prisma.d.ts"],
}
```
Finally, configure jest-prisma environment using `setupFilesAfterEnv`:
```js
/* jest.config.mjs */
export default {
testEnvironment: "@quramy/jest-prisma/environment",
setupFilesAfterEnv: ["setupAfterEnv.ts"],
};
```
```js
/* setupAfterEnv.ts */
import { prisma } from "./src/client";
jestPrisma.initializeClient(prisma);
```
## Tips
### Singleton
If your project uses singleton Prisma client instance, such as:
```ts
/* src/client.ts */
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
```
```ts
/* src/userService.ts */
import { prisma } from "./client.ts";
export function findUserById(id: string) {
const result = await prisma.user.findUnique({
where: { id },
});
return result;
}
```
You can replace the singleton instance to `jestPrisma.client` via `jest.mock`.
```js
/* setup-prisma.js */
jest.mock("./src/client", () => {
return {
prisma: jestPrisma.client,
};
});
```
```js
/* jest.config.mjs */
export default {
testEnvironment: "@quramy/jest-prisma/environment",
setupFilesAfterEnv: ["/setup-prisma.js"],
};
```
```ts
import { prisma } from "./client";
import { findUserById } from "./userService";
describe("findUserById", () => {
beforeEach(async () => {
await prisma.user.create({
data: {
id: "test_user_id",
},
});
});
it("should return user", async () => {
await findUserById("test_user_id");
// assertion
});
});
```
### DI Containers
If you're using DI containers such as [InversifyJS](https://github.com/inversify/InversifyJS) or [Awilix](https://github.com/jeffijoe/awilix) and wish to introduce jest-prisma, you can easily do that just by rebinding PrismaClient to a global `jestPrisma` instance provided by jest-prisma.
Here is an example below. Given that we have the following repository. Note that it is decorated by `@injectable` so will `prisma` will be inject as a constructor argument.
```ts
/* types.ts */
export const TYPES = {
PrismaClient: Symbol.for("PrismaClient"),
UserRepository: Symbol.for("UserRepository"),
};
```
```ts
/* user-repository.ts */
import { TYPES } from "./types";
interface IUserRepository {
findAll(): Promise;
findById(): Promise;
save(): Promise;
}
@injectable()
class UserRepositoryPrisma implements IUserRepository {
constructor(
@inject(TYPES.PrismaClient)
private readonly prisma: PrismaClient,
) {}
async findAll() { .. }
async findById() { .. }
async save() { .. }
}
```
```ts
/* inversify.config.ts */
import { Container } from "inversify";
import { PrismaClient } from "prisma";
import { TYPES } from "./types";
import { UserRepositoryPrisma, IUserRepository } from "./user-repository";
const container = new Container();
container.bind(TYPES.PrismaClient).toConstantValue(new PrismaClient());
container.bind(TYPES.UserRepository).to(UserRepositoryPrisma);
```
In most cases, the setup above allows you to inject a pre-configured `PrismaClient` by associating the symbol to an actual instance like `bind(TYPES.PrismaClient).toConstantValue(new PrismaClient())` and then acquire the repository by `get(TYPES.UserRepository)`.
However, with jest-prisma, the global `jestPrisma.client` object is initialised for each unit tests so you have to make sure that you're binding the instance _after_ the initialisation.
Note that we're rebinding PrismaClient to the jest-prisma inside `beforeEach` phase. Any other phase including `beforeAll` or `setupFilesAfterEnv` may not work as you expect.
```ts
/* user-repository.spec.ts */
describe("UserRepository", () => {
beforeEach(() => {
container
.rebind(TYPES.PrismaClient)
.toConstantValue(jestPrisma.client);
});
it("creates a user" ,() => {
constainer.get(TYPES.UserRepository);
...
});
});
```
### Workaround for DateTime invocation error
If you encounter errors like the following:
```
Argument gte: Got invalid value {} on prisma.findFirstUser. Provided Json, expected DateTime.
```
It's because that Jest global `Date` is differ from JavaScript original `Date`(https://github.com/facebook/jest/issues/2549).
And this error can be work around by using [single context environment](https://www.npmjs.com/package/jest-environment-node-single-context):
```ts
/* myEnv.ts */
import type { Circus } from "@jest/types";
import type { JestEnvironmentConfig, EnvironmentContext } from "@jest/environment";
import { PrismaEnvironmentDelegate } from "@quramy/jest-prisma-core";
import Environment from "jest-environment-node-single-context";
export default class PrismaEnvironment extends Environment {
private readonly delegate: PrismaEnvironmentDelegate;
constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
super(config, context);
this.delegate = new PrismaEnvironmentDelegate(config, context);
}
async setup() {
const jestPrisma = await this.delegate.preSetup();
await super.setup();
this.global.jestPrisma = jestPrisma;
}
handleTestEvent(event: Circus.Event) {
return this.delegate.handleTestEvent(event);
}
async teardown() {
await Promise.all([super.teardown(), this.delegate.teardown()]);
}
}
```
```js
/* jest.config.mjs */
export default {
testEnvironment: "myEnv.ts",
};
```
Caveat: This work around might me affect your test cases using Jest fake timer features.
See also https://github.com/Quramy/jest-prisma/issues/56.
### Transaction Rollback
If you are using [$transaction callbacks in Prisma with the feature to roll back in case of an error](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#:~:text=If%20your%20application%20encounters%20an%20error%20along%20the%20way%2C%20the%20async%20function%20will%20throw%20an%20exception%20and%20automatically%20rollback%20the%20transaction.), that's ok too. :D
Set `enableExperimentalRollbackInTransaction` in testEnvironmentOptions to `true`. This option allows nested transaction.
```js
/* jest.config.mjs */
export default {
testEnvironment: "@quramy/jest-prisma/environment",
testEnvironmentOptions: {
enableExperimentalRollbackInTransaction: true, // <- add this
},
};
```
Then, jest-prisma reproduces them in tests
```ts
const someTransaction = async prisma => {
await prisma.$transaction(async p => {
await p.user.create({
data: {
// ...
},
});
throw new Error("Something failed. Affected changes will be rollback.");
});
};
it("test", async () => {
const prisma = jestPrisma.client;
const before = await prisma.user.aggregate({ _count: true });
expect(before._count).toBe(0);
await someTransaction(prisma);
const after = await prisma.user.aggregate({ _count: true });
expect(after._count).toBe(0); // <- this will be 0
});
```
> [!TIP]
> The nested transaction is used to suppress PostgreSQL's `current transaction is aborted commands ignored until end of transaction block` error. See https://github.com/Quramy/jest-prisma/issues/141 if you want more details.
Internally, SAVEPOINT, which is formulated in the Standard SQL, is used.
Unfortunately, however, MongoDB does not support partial rollbacks within a Transaction using SAVEPOINT, so MongoDB is not able to reproduce rollbacks. In this case, do not set `enableExperimentalRollbackInTransaction` to true.
## References
### `global.jestPrisma`
```ts
export interface JestPrisma {
/**
*
* Primsa Client Instance whose transaction are isolated for each test case.
* And this transaction is rolled back automatically after each test case.
*
*/
readonly client: T;
/**
*
* You can call this from setupAfterEnv script and set your customized PrismaClient instance.
*
*/
readonly initializeClient: (client: unknown) => void;
}
```
### Environment options
```ts
export interface JestPrismaEnvironmentOptions {
/**
*
* If set true, each transaction is not rolled back but committed.
*
*/
readonly disableRollback?: boolean;
/**
*
* If set to true, it will reproduce the rollback behavior when an error occurs at the point where the transaction is used.
*
* In particular, if you are using MongoDB as the Database connector, you must not set it to true.
*
*/
readonly enableExperimentalRollbackInTransaction?: boolean;
/**
*
* Display SQL queries in test cases to STDOUT.
*
*/
readonly verboseQuery?: boolean;
/**
*
* The maximum amount of time the Prisma Client will wait to acquire a transaction from the database.
*
* The default value is 5 seconds.
*
*/
readonly maxWait?: number;
/**
*
* The maximum amount of time the interactive transaction can run before being canceled and rolled back.
*
* The default value is 5 seconds.
*
*/
readonly timeout?: number;
/**
*
* Sets the transaction isolation level. By default this is set to the value currently configured in your database.
*
* @link https://www.prisma.io/docs/orm/prisma-client/queries/transactions#transaction-isolation-level
*
*/
readonly isolationLevel?: Prisma.TransactionIsolationLevel;
/**
*
* Override the database connection URL.
*
* Useful if you have a separate database for testing.
*
*/
readonly databaseUrl?: string;
}
```
## License
MIT