jest-prisma
Jest environment for Prisma integrated testing.
You can run each test case in isolated transaction which is rolled back automatically.
How to use
Install
$ npm i @quramy/jest-prisma -D
Configure Jest
export default {
testEnvironment: "@quramy/jest-prisma/environment",
};
Configure TypeScript
{
"compilerOptions": {
"types": ["@types/jest", "@quramy/jest-prisma"],
}
}
Configure Prisma
jest-prisma uses Prisma interactive transaction feature. 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
describe(UserService, () => {
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
.
export default {
testEnvironment: "@quramy/jest-prisma/environment",
testEnvironmentOptions: {
verboseQuery: true,
},
};
Alternatively, you can use @jest-environment-options
pragma in your test file:
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:
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:
import type { JestPrisma } from "@quramy/jest-prisma-core";
import type { prisma } from "../src/client";
declare global {
var jestPrisma: JestPrisma<typeof prisma>;
}
And add the path of this declaration to your tsconfig.json:
{
"compilerOptions": {
"types": ["@types/jest"],
},
"includes": ["typeDefs/jest-prisma.d.ts"],
}
Finally, configure jest-prisma environment using setupFilesAfterEnv
:
export default {
testEnvironment: "@quramy/jest-prisma/environment",
setupFilesAfterEnv: ["setupAfterEnv.ts"],
};
import { prisma } from "./src/client";
jestPrisma.initializeClient(prisma);
Tips
Singleton
If your project uses singleton Prisma client instance, such as:
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
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
.
jest.mock("./src/client", () => {
return {
prisma: jestPrisma.client,
};
});
export default {
testEnvironment: "@quramy/jest-prisma/environment",
setupFilesAfterEnv: ["<rootDir>/setup-prisma.js"],
};
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");
});
});
DI Containers
If you're using DI containers such as InversifyJS or 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.
export const TYPES = {
PrismaClient: Symbol.for("PrismaClient"),
UserRepository: Symbol.for("UserRepository"),
};
import { TYPES } from "./types";
interface IUserRepository {
findAll(): Promise<User[]>;
findById(): Promise<User[]>;
save(): Promise<User[]>;
}
@injectable()
class UserRepositoryPrisma implements IUserRepository {
constructor(
@inject(TYPES.PrismaClient)
private readonly prisma: PrismaClient,
) {}
async findAll() { .. }
async findById() { .. }
async save() { .. }
}
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<IUserRepository>(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.
describe("UserRepository", () => {
beforeEach(() => {
container
.rebind(TYPES.PrismaClient)
.toConstantValue(jestPrisma.client);
});
it("creates a user" ,() => {
constainer.get<IUserRepository>(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:
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()]);
}
}
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, that's ok too. :D
Set enableExperimentalRollbackInTransaction
in testEnvironmentOptions to true
. This option allows nested transaction.
export default {
testEnvironment: "@quramy/jest-prisma/environment",
testEnvironmentOptions: {
enableExperimentalRollbackInTransaction: true,
},
};
Then, jest-prisma reproduces them in tests
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);
});
[!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
export interface JestPrisma<T = PrismaClientLike> {
readonly client: T;
readonly initializeClient: (client: unknown) => void;
}
Environment options
export interface JestPrismaEnvironmentOptions {
readonly disableRollback?: boolean;
readonly enableExperimentalRollbackInTransaction?: boolean;
readonly verboseQuery?: boolean;
readonly maxWait?: number;
readonly timeout?: number;
readonly isolationLevel?: Prisma.TransactionIsolationLevel;
readonly databaseUrl?: string;
}
License
MIT