How to deep mock Prisma Client in JavaScript (not TypeScript)? - jestjs

I'm want to unit test Next.js API route with Prisma in JavaScript. Unfortunately, the Prisma's unit testing guide is written for Typescript.
I have jest.config.js which will setup the mock in jest.setup.js
const nextJest = require("next/jest");
const createJestConfig = nextJest({
dir: "./",
});
const config = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
moduleDirectories: ["node_modules", "<rootDir>/"],
moduleNameMapper: {
"#/(.*)$": "<rootDir>/$1",
},
};
module.exports = createJestConfig(config);
and this is how I configured the mock in jest.setup.js
import prisma from "#/utils/client";
jest.mock("#/utils/client", () => ({
__esModule: true,
default: {
user: {
findMany: jest.fn(),
},
// ... and each and everyone of the entities
},
}));
export const prismaMock = prisma;
and the following test case passed
describe("Calculator", () => {
it("renders a calculator", async () => {
await prismaMock.user.findMany.mockResolvedValue(["abc]);
const { req, res } = createMocks();
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getData()).toBe('["abc"]');
});
});
With this approach, I have to mock each and everyone of the models and function in jest.setup.js. Is there a way to mock all the models and functions automatically? Is there a similar JavaScript library which provides mockDeep from jest-mock-extended?

Related

Fastify CLI decorators undefined

I'm using fastify-cli for building my server application.
For testing I want to generate some test JWTs. Therefore I want to use the sign method of the fastify-jwt plugin.
If I run the application with fastify start -l info ./src/app.js everything works as expected and I can access the decorators.
But in the testing setup I get an error that the jwt decorator is undefined. It seems that the decorators are not exposed and I just can't find any error. For the tests I use node-tap with this command: tap \"test/**/*.test.js\" --reporter=list
app.js
import { dirname, join } from 'path'
import autoload from '#fastify/autoload'
import { fileURLToPath } from 'url'
import jwt from '#fastify/jwt'
export const options = {
ignoreTrailingSlash: true,
logger: true
}
export default async (fastify, opts) => {
await fastify.register(jwt, {
secret: process.env.JWT_SECRET
})
// autoload plugins and routes
await fastify.register(autoload, {
dir: join(dirname(fileURLToPath(import.meta.url)), 'plugins'),
options: Object.assign({}, opts),
forceESM: true,
})
await fastify.register(autoload, {
dir: join(dirname(fileURLToPath(import.meta.url)), 'routes'),
options: Object.assign({}, opts),
forceESM: true
})
}
helper.js
import { fileURLToPath } from 'url'
import helper from 'fastify-cli/helper.js'
import path from 'path'
// config for testing
export const config = () => {
return {}
}
export const build = async (t) => {
const argv = [
path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'app.js')
]
const app = await helper.build(argv, config())
t.teardown(app.close.bind(app))
return app
}
root.test.js
import { auth, build } from '../helper.js'
import { test } from 'tap'
test('requests the "/" route', async t => {
t.plan(1)
const app = await build(t)
const token = app.jwt.sign({ ... }) //-> jwt is undefined
const res = await app.inject({
method: 'GET',
url: '/'
})
t.equal(res.statusCode, 200, 'returns a status code of 200')
})
The issue is that your application diagram looks like this:
and when you write const app = await build(t) the app variable points to Root Context, but Your app.js contains the jwt decorator.
To solve it, you need just to wrap you app.js file with the fastify-plugin because it breaks the encapsulation:
import fp from 'fastify-plugin'
export default fp(async (fastify, opts) => { ... })
Note: you can visualize this structure by using fastify-overview (and the fastify-overview-ui plugin together:

How to override url for RTK query

I'm writing pact integration tests which require to perform actual call to specific mock server during running tests.
I found that I cannot find a way to change RTK query baseUrl after initialisation of api.
it('works with rtk', async () => {
// ... setup pact expectations
const reducer = {
[rtkApi.reducerPath]: rtkApi.reducer,
};
// proxy call to configureStore()
const { store } = setupStoreAndPersistor({
enableLog: true,
rootReducer: reducer,
isProduction: false,
});
// eslint-disable-next-line #typescript-eslint/no-explicit-any
const dispatch = store.dispatch as any;
dispatch(rtkApi.endpoints.GetModules.initiate();
// sleep for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
const data = store.getState().api;
expect(data.queries['GetModules(undefined)']).toEqual({modules: []});
});
Base api
import { createApi } from '#reduxjs/toolkit/query/react';
import { graphqlRequestBaseQuery } from '#rtk-query/graphql-request-base-query';
import { GraphQLClient } from 'graphql-request';
export const client = new GraphQLClient('http://localhost:12355/graphql');
export const api = createApi({
baseQuery: graphqlRequestBaseQuery({ client }),
endpoints: () => ({}),
});
query is very basic
query GetModules {
modules {
name
}
}
I tried digging into customizing baseQuery but were not able to get it working.

Can I apply 'setupFilesAfterEnv' to specific file in jest config?

I'm working on appling prisma unit testing and Integration testing
I want to apply unit testing for *.service.test.ts files
and intergration testing for *.test.ts files.
I followed the Prisma document, but there is something that doesn't work.
singleton.ts
import { mockReset, mockDeep, DeepMockProxy } from "jest-mock-extended";
import { PrismaClient } from "#prisma/client";
import Prisma from "../src/db/prisma";
jest.mock("../src/db/prisma", () => {
return {
__esModule: true,
default: mockDeep<PrismaClient>(),
};
});
beforeEach(() => {
// eslint-disable-next-line no-use-before-define
mockReset(prismaMock);
});
export const prismaMock = Prisma as unknown as DeepMockProxy<PrismaClient>;
jest.config.ts
When turing off setupFilesAfterEnv option, testing *.test.ts files are working.
So I Want turn off setupFilesAfterEnv option in Integration testing
Is it applicable only when unit testing?
...
setupFilesAfterEnv: [
"./jest/singleton.ts"
]
I think your question is a bit incomplete, but I might know what you are talking about because I am running into a similar problem.
If you are trying to do the integration tests from prisma documentation, you need to unmock your prisma client on your integration tests. Otherwise it will still be mocked by your singleton.ts file
something like this:
myTest.test.js (integration test file)
jest.unmock("../src/db/prisma");
Another way of doing it, is just to remove the singleton from setupFilesAfterEnv and just import the prisma client from the singleton file inside your tests.
What I did:
I created 2 tests files (one for integration and another one for unit testing: CreateData.unit.test.ts and CreateData.int.test. I also created 2 singleton files:
singleton.unit.ts (I wanted that to be applied on my unit tests)
import { PrismaClient } from '#prisma/client';
import { mockDeep, mockReset, DeepMockProxy, mock } from 'jest-mock-extended';
import prismaClient from '../prismaClient';
jest.mock('../prismaClient', () => ({
__esModule: true,
default: mockDeep<PrismaClient>(),
}));
beforeEach(() => {
mockReset(prismaMock);
});
export const prismaMock = prismaClient as unknown as DeepMockProxy<PrismaClient>;
singleton.int.ts (I wanted that applied in my integration tests)
import prismaClient from '../prismaClient';
afterAll(async () => {
const deleteData = prismaClient.data.deleteMany();
await prismaClient.$transaction([
deleteData,
]);
await prismaClient.$disconnect();
});
export { prismaClient };
I removed setupFilesAfterEnv from jest.config.js
Then create your unit tests and integration tests. You don't need to unmock prisma client if you removed the singleton from setupFilesAfterEnv in jest.config.ts
myTest.unit.test.ts
import { prismaMock } from "<path>/singleton.unit";
import { CreateData } from "<path>/CreateData";
let createData;
let createDate = new Date();
const data = {
id: "randomId1234",
name: "Bob Singer",
email: "bob#gmail.com",
password: "123456",
};
beforeEach(() => {
createData = new CreateData();
});
describe('CreateData', () => {
it("should create new data", async () => {
const result = createData.execute(data);
prismaMock.data.create.mockResolvedValue(data);
await expect(result).resolves.toEqual({
id: "randomId1234",
name: "Bob Singer",
email: "bob#gmail.com",
password: "123456",
});
});
});
myTest.int.test.ts
import prismaClient from "<path>/singleton.int";
import { CreateData } from "<path>/CreateData"
let createData;
let createDate = new Date();
const data = {
id: "randomId1234",
name: "Bob Singer",
email: "bob#gmail.com",
password: "123456",
};
beforeEach(() => {
createData = new CreateData();
});
describe('CreateTrainer', () => {
it("should create new trainer", async () => {
const result = await createData.execute(data);
const newData = await prismaClient.data.findUnique({
where: {
email: "bob#gmail.com"
}
});
console.log(result);
expect(newData?.email).toEqual(data.email);
});
});

How to turn of module reloading in JEST?

I am using Jest to testing express REST api written in express.js and typescript using ts-jest. My problem is Jest loads app module (express) in every test suite, it lasts only 3-4 seconds, but there is ~80 test suites each containing a multiple test cases.
My first thought was to remove jest.resetModules() from global afterAll() function, but it didn't helped. Is there another way how to change this behaviour or it is feature by design?
setup.ts
import { sequelize } from '../src/db/models'
import seeds from '../src/db/seeders/test'
// multiple mocks of services
jest.mock('../some/custom/module/mocks')
beforeAll(async () => {
await sequelize.sync({ force: true }) // basically drop db and create clean one
await seeds() // seed database with data
})
afterAll(async () => {
jest.clearAllMocks()
// jest.resetModules() //! This line was removed
await sequelize.close()
global.gc()
})
jest.setTimeout(10000)
global.ts
import { createJwt } from '../src/utils/authorization'
export default async () => {
// create some acces tokens and add to process.env
process.env.jwttoken = await createJwt({ uid: 1 }, { audience: 'users' })
}
jest.config.js
module.exports = {
transform: {
'^.+\\.ts?$': 'ts-jest'
},
roots: [
'<rootDir>/tests/'
],
moduleFileExtensions: [
'ts',
'js'
],
setupFilesAfterEnv: [
'<rootDir>/tests/setup.ts'
],
globalSetup: '<rootDir>/tests/global.ts',
testEnvironment: 'node'
}
example test
import supertest from 'supertest'
import app from '../../../../../src/app'
describe(`[GET] ${endpoint(':id')})`, () => {
const request = supertest(app). // every time jest hits this line in test, it load app again
it('no authorization token | code 401', async () => {
const response = await request.get('/something')
.set('Content-Type', 'application/json')
expect(response.status).toBe(401)
})
start script
POSTGRESQL_URL=postgresql://postgres:root#127.0.0.1:5432/database JWT_SECRET=secret node --expose-gc \"./node_modules/jest/bin/jest.js\" --runInBand --passWithNoTests --logHeapUsage

How to mock typeorm connection

In integration tests I am using the following snippets to create connection
import {Connection, createConnection} from 'typeorm';
// #ts-ignore
import options from './../../../ormconfig.js';
export function connectDb() {
let con: Connection;
beforeAll(async () => {
con = await createConnection(options);
});
afterAll(async () => {
await con.close();
});
}
I am trying to unit test a class which calls typeorm repository in one of its method and without call that helper function connectDb() above I get the following error which is expected of course.
ConnectionNotFoundError: Connection "default" was not found.
My question is how can I mock connection. I have tried the following without any success
import typeorm, {createConnection} from 'typeorm';
// #ts-ignore
import options from "./../../../ormconfig.js";
const mockedTypeorm = typeorm as jest.Mocked<typeof typeorm>;
jest.mock('typeorm');
beforeEach(() => {
//mockedTypeorm.createConnection.mockImplementation(() => createConnection(options)); //Failed
mockedTypeorm.createConnection = jest.fn().mockImplementation(() => typeorm.Connection);
MethodRepository.prototype.changeMethod = jest.fn().mockImplementation(() => {
return true;
});
});
Running tests with that kind of mocking gives this error
TypeError: decorator is not a function
Note: if I call connectDb() in tests everything works fine. But I don't want to do that since it takes too much time as some data are inserted into db before running any test.
Some codes have been omitted for simplicity. Any help will be appreciated
After a bunch of research and experiment I've ended up with this solution. I hope it works for someone else who experienced the same issue...
it does not need any DB connection
testing service layer content, not the DB layer itself
test can cover all the case I need to test without hassle, I just need to provide the right output to related typeorm methods.
This is the method I want to test
#Injectable()
export class TemplatesService {
constructor(private readonly templatesRepository: TemplatesRepository) {}
async list(filter: ListTemplatesReqDTO) {
const qb = this.templatesRepository.createQueryBuilder("tl");
const { searchQuery, ...otherFilters } = filter;
if (filter.languages) {
qb.where("tl.language IN (:...languages)");
}
if (filter.templateTypes) {
qb.where("tl.templateType IN (:...templateTypes)");
}
if (searchQuery) {
qb.where("tl.name LIKE :searchQuery", { searchQuery: `%${searchQuery}%` });
}
if (filter.skip) {
qb.skip(filter.skip);
}
if (filter.take) {
qb.take(filter.take);
}
if (filter.sort) {
qb.orderBy(filter.sort, filter.order === "ASC" ? "ASC" : "DESC");
}
return qb.setParameters(otherFilters).getManyAndCount();
}
...
}
This is the test:
import { SinonStub, createSandbox, restore, stub } from "sinon";
import * as typeorm from "typeorm";
describe("TemplatesService", () => {
let service: TemplatesService;
let repo: TemplatesRepository;
const sandbox = createSandbox();
const connectionStub = sandbox.createStubInstance(typeorm.Connection);
const templatesRepoStub = sandbox.createStubInstance(TemplatesRepository);
const queryBuilderStub = sandbox.createStubInstance(typeorm.SelectQueryBuilder);
stub(typeorm, "createConnection").resolves((connectionStub as unknown) as typeorm.Connection);
connectionStub.getCustomRepository
.withArgs(TemplatesRepository)
.returns((templatesRepoStub as unknown) as TemplatesRepository);
beforeAll(async () => {
const builder: TestingModuleBuilder = Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
database: "test",
entities: [Template],
synchronize: true,
dropSchema: true
})
],
providers: [ApiGuard, TemplatesService, TemplatesRepository],
controllers: []
});
const module = await builder.compile();
service = module.get<TemplatesService>(TemplatesService);
repo = module.get<TemplatesRepository>(TemplatesRepository);
});
beforeEach(async () => {
// do something
});
afterEach(() => {
sandbox.restore();
restore();
});
it("Service should be defined", () => {
expect(service).toBeDefined();
});
describe("list", () => {
let fakeCreateQueryBuilder;
it("should return records", async () => {
stub(queryBuilderStub, "skip" as any).returnsThis();
stub(queryBuilderStub, "take" as any).returnsThis();
stub(queryBuilderStub, "sort" as any).returnsThis();
stub(queryBuilderStub, "setParameters" as any).returnsThis();
stub(queryBuilderStub, "getManyAndCount" as any).resolves([
templatesRepoMocksListSuccess,
templatesRepoMocksListSuccess.length
]);
fakeCreateQueryBuilder = stub(repo, "createQueryBuilder" as any).returns(queryBuilderStub);
const [items, totalCount] = await service.list({});
expect(fakeCreateQueryBuilder.calledOnce).toBe(true);
expect(fakeCreateQueryBuilder.calledOnce).toBe(true);
expect(items.length).toBeGreaterThan(0);
expect(totalCount).toBeGreaterThan(0);
});
});
});
cheers!

Resources