NestJs Testing create a single instance of app across all tests - nestjs

I am running into the issue Error querying the database: db error: FATAL: sorry, too many clients already and I am convinced it is because a new instance of the app is being instantiated for every test suite. I have attempted to break the app creation out into a helper file, and that file looks as follows
import { INestApplication } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { AppModule } from '../../src/app.module';
import { PrismaService } from '../../src/prisma.service';
declare global {
var app: INestApplication | undefined;
}
export const getApp = async () => {
if (global.app) {
return global.app;
}
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
providers: [PrismaService],
}).compile();
const app = moduleFixture.createNestApplication();
await app.init();
global.app = app;
return app;
};
This however does not work, when I add console logs, I can see that app is being instantiated for every test suite.
This is how my typical before hook looks
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});

I had the same issue and solved it by using Jest's globalSetup and globalTeardown options. I create one instance of the app and seed the DB before running the tests, and destroy it and run the teardown when they've finished. I use a global variable to reference the app instance.
In my jest-e2e.json:
{
...
"globalSetup": "<rootDir>/test/test-setup.ts",
"globalTeardown": "<rootDir>/test/test-teardown.ts"
}
test-setup.ts:
import * as fs from 'fs';
import { FastifyAdapter, NestFastifyApplication } from '#nestjs/platform-fastify';
import { Test } from '#nestjs/testing';
import { AppModule } from './../src/app.module';
import { WriteDataSource } from './../src/database/database.module';
module.exports = async () => {
const moduleRef = await Test.createTestingModule({
imports: [ AppModule ]
})
.compile();
global.app = moduleRef.createNestApplication<NestFastifyApplication>(
new FastifyAdapter()
);
await global.app.init();
await global.app.getHttpAdapter().getInstance().ready();
const seedQuery = fs.readFileSync(__dirname + '/scripts/database-seed.sql', { encoding: 'utf-8' });
await WriteDataSource.manager.query(seedQuery);
};
test-teardown.ts:
import * as fs from 'fs';
import { WriteDataSource } from '../src/database/database.module';
module.exports = async () => {
const teardownQuery = fs.readFileSync(__dirname + '/scripts/database-teardown.sql', { encoding: 'utf-8' }).replace(/\n/g, '');
await WriteDataSource.query(teardownQuery);
await WriteDataSource.destroy();
await global.app.close();
};

I solved it by exporting the server instance and everything else i needed:
let server: Server
let app: INestApplication
let testService: TestService
export const initServer = async () => {
const module = Test.createTestingModule({
imports: [AppModule, TestModule]
})
const testModule = await module.compile()
app = testModule.createNestApplication()
app.useGlobalPipes(
new ValidationPipe({
whitelist: true
})
)
server = app.getHttpServer()
testService = app.get(TestService)
await app.init()
}
export { app, server, testService }
Here i exported the function to clear all my databases to use where needed:
export const clearAllDatabases = async () => {
models.map(async (model: any) => {
await model.destroy({
where: {},
truncate: true,
cascade: true
})
})
}
import { app, initServer } from '#tests/resources/config/test-server'
import { clearAllDatabases } from '#tests/resources/config/clear-databases'
export const setupTestData = async () => {
await initServer()
await clearAllDatabases()
await userRegister()
await app.close()
}

Related

Testing firestore with jest not working as expected

I have the following service which I just need to test that whenever an user is not validated, we throw an error
async payoutPendingRewards(userId: string): Promise<Reward[]> {
const db = admin.firestore();
const userRef = db.collection('UserProfiles').doc(userId);
const doc = await userRef.get();
if (!doc.data() || !doc.data().validated) {
throw Error('User profile has not been validated');
}
const userBankAccount = await this.getUserPayoutAccount(userId);
if (userBankAccount) {
const pendingRewards = await this.getPendingRewards(userId);
const amount = pendingRewards.reduce(
(amount, reward) => amount + reward.amount,
0,
);
if (amount > 0) {
try {
const payoutId = await this.createPayout(
userBankAccount.accountType,
userBankAccount.accountId,
amount,
);
if (payoutId) {
// TODO: add payout ID to Reward
return await this.updatePendingRewards(pendingRewards);
}
} catch (e) {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: 'There was an error: Payout Not Created',
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
When trying to test it using jest, I can't get to mock the firebase-admin functions. I get a firebase error when trying to do it
import { Test, TestingModule } from '#nestjs/testing';
import { HttpModule, HttpService } from '#nestjs/axios';
import { describe, expect, beforeEach, it, jest } from '#jest/globals';
import { INestApplication, forwardRef } from '#nestjs/common';
import { SecretManagerServiceClient } from '#google-cloud/secret-manager';
import * as request from 'supertest';
import { RewardsService } from '../src/rewards/rewards.service';
import { RewardsController } from '../src/rewards/rewards.controller';
import { UsersModule } from '../src/users/users.module';
import { FirebaseAuthGuard } from '../src/auth/firebase-auth.guard';
import { HttpExceptionFilter } from '../src/exceptions/http-exception.filter';
import { TransformInterceptor } from '../src/interceptors/transform.interceptor';
import { firestore } from './setupFirestoreTests';
describe('RewardsService', () => {
let service: RewardsService;
let httpService: HttpService;
let app: INestApplication;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RewardsService],
controllers: [RewardsController],
imports: [
FirebaseAuthGuard,
HttpExceptionFilter,
TransformInterceptor,
HttpModule,
forwardRef(() => UsersModule),
],
}).compile();
app = module.createNestApplication();
service = module.get<RewardsService>(RewardsService);
httpService = module.get<HttpService>(HttpService);
await app.init();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('makes a call to firestore', async () => {
service.payoutPendingRewards('123');
expect(firestore().doc().get).toHaveBeenCalledWith('123');
});
});
I need help just mocking the firebase functions. Already tried by creating mock functions on a different file but it didn't work

E2E Testing NestJS

Im new to NestJS and im trying to setup my end to end testing. It does not throw any errors but the request always returns 404.
The test look like this:
import { CreateProductDto } from './../src/products/dto/create-product.dto';
import { ProductsModule } from './../src/products/products.module';
import { Product } from './../src/products/entities/product.entity';
import { Repository } from 'typeorm';
import { INestApplication } from '#nestjs/common';
import * as request from 'supertest';
import { createTypeOrmConfig } from './utils';
import { Test, TestingModule } from '#nestjs/testing';
import { TypeOrmModule } from '#nestjs/typeorm';
describe('ProductsController (e2e)', () => {
let app: INestApplication;
let productRepository: Repository<Product>;
beforeEach(async () => {
const config = createTypeOrmConfig();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(config),
TypeOrmModule.forFeature([Product]),
ProductsModule,
],
}).compile();
app = moduleFixture.createNestApplication();
productRepository = moduleFixture.get('ProductRepository');
await app.init();
});
afterAll(async () => {
await app.close();
});
it('create', async () => {
// Arrange
const createProductDto = new CreateProductDto();
createProductDto.name = 'testname';
createProductDto.description = 'testdescription';
createProductDto.price = 120;
createProductDto.image = 'testimage';
// Act
const response = await request(app.getHttpServer())
.post(`/api/v1/products`)
.send(createProductDto);
// Assert
expect(response.status).toEqual(201);
});
});
I expected it to returns a 201 response. I tried more routes and same as before.
Do you have #Controller('api/v1/products') on your ProductsController? If not, you don't ever configure the test application to be served from /api/v1, you'd need to set the global prefix and enable versioning (assuming you usually do these in your main.ts). For a simple fix, remove the /api/v1 from your .post() method.

Increase body limit with nestjs & fastify

My main.ts looks like this :
import { NestFactory } from '#nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '#nestjs/platform-fastify';
import { Logger } from 'nestjs-pino';
import { processRequest } from 'graphql-upload';
import { AppModule } from './app.module';
async function bootstrap() {
const adapter = new FastifyAdapter();
const fastifyInstance = adapter.getInstance();
fastifyInstance.addContentTypeParser('multipart', (request, done) => {
request.isMultipart = true;
done();
});
fastifyInstance.addHook('preValidation', async (request: any, reply) => {
if (!request.raw.isMultipart) {
return;
}
request.body = await processRequest(request.raw, reply.raw);
});
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
{ bufferLogs: true },
);
app.useLogger(app.get(Logger));
app.enableCors();
await app.listen(parseInt(process.env.SERVER_PORT || '3000', 10), '0.0.0.0');
}
bootstrap();
According to the fastify doc the body limit is 1MiB by default, however I want it to be larger. So I tried like this :
const adapter = new FastifyAdapter({ bodyLimit: 124857600 }); but I still get the same problem with my payload being too large.
Try to add this when you are creating the app
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ bodyLimit: 10048576 }),

How to test Models (Mongoose) in a service (NestJS) with jest

I have a backend done with NestJS. In my service I inject two Mongoose Models. I use Jest to test the service.
Models are declared as is and injected into the module:
quizes.providers.ts
import { Connection } from 'mongoose';
import { QuizSchema } from './schemas/quiz.schema';
export const quizesProviders = [
{
provide: 'CLASS_MODEL',
useFactory: (connection: Connection) => connection.model('Quiz', QuizSchema),
inject: ['DATABASE_CONNECTION'],
},
];
users.providers.ts
import { Connection } from 'mongoose';
import { UserSchema } from './schemas/user.schema';
export const usersProviders = [
{
provide: 'USER_MODEL',
useFactory: (connection: Connection) => connection.model('User', UserSchema),
inject: ['DATABASE_CONNECTION'],
},
];
Example of module:
quizes.module.ts
import { Module } from '#nestjs/common';
import { QuizesController } from './quizes.controller';
import { QuizesService } from './quizes.service';
import { quizesProviders } from './quizes.providers';
import { usersProviders } from '../auth/users.providers';
import { DatabaseModule } from 'src/database.module';
import { AuthModule } from 'src/auth/auth.module';
#Module({
imports: [DatabaseModule, AuthModule],
controllers: [QuizesController],
providers: [QuizesService,
...quizesProviders, ...usersProviders]
})
export class QuizesModule {}
Then in my service, I inject models:
quizes.service.ts
#Injectable()
export class QuizesService {
constructor(
#Inject('CLASS_MODEL')
private classModel: Model<Quiz>,
#Inject('USER_MODEL')
private userModel: Model<User>
) {}
In my quizes.spec.ts (jest) I began to do things like that. It compiles but doesn't work:
import { Test } from '#nestjs/testing';
import * as mongoose from 'mongoose';
import { User } from 'src/auth/user.interface';
import { Quiz } from './quiz.interface';
import { databaseProviders } from '../database.providers';
const USER_MODEL:mongoose.Model<User> = mongoose.model('User', UserSchema);
const CLASS_MODEL:mongoose.Model<Quiz> = mongoose.model('Quiz', QuizSchema);
const mockingQuizModel = () => {
find: jest.fn()
}
const mockingUserModel = () => {
find: jest.fn()
}
const mockUser = {
username: 'Test user'
}
describe('QuizesService', () => {
let quizesService;
let userModel , classModel;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [QuizesService, ...usersProviders, ...quizesProviders,...databaseProviders,
{provide: USER_MODEL, useFactory: mockingUserModel},
{provide: CLASS_MODEL, useFactory: mockingQuizModel},
],
}).compile();
quizesService = await module.get<QuizesService>(QuizesService);
classModel = await module.get<mongoose.Model<Quiz>>(CLASS_MODEL)
userModel = await module.get<mongoose.Model<User>>(USER_MODEL)
})
describe('getAllQuizes', ()=> {
it('get all quizes', () => {
expect(userModel.find).not.toHaveBeenCalled();
})
})
})
userModel is undefined and the test does not exit.
Use the getModelToken function as defined in NestJS official: https://docs.nestjs.com/v6/
Techniques -> Mongo (Scroll down to Testing section)
Then your code should look a bit like this:
import { getModelToken } from '#nestjs/mongoose';
const mockRepository = {
find() {
return {};
}
};
const module = await Test.createTestingModule({
providers: [ ...,
{provide: getModelToken('CLASS_MODEL'), useValue: mockRepository,},
{provide: getModelToken('USER_MODEL'), useValue: mockRepository,},
],
...
Fixed
You should not use await for module.get
quizesService = module.get<QuizesService>(QuizesService);
clientClassModel = module.get(getModelToken('CLASS_MODEL'))
clientUserModel = module.get(getModelToken('USER_MODEL'))
The setup of the test suite was ok but not the test
I test the service getAllQuizes method
Here is the service
#Injectable()
export class QuizesService {
constructor(
#InjectModel('CLASS_MODEL')
private classModel: Model<Quiz>,
#InjectModel('USER_MODEL')
private userModel: Model<User>
) {}
async getAllQuizes(user: User) : Promise<Quiz[]> {
// console.log(user);
let userId;
try {
const userEntity = await this.userModel.find({username: user.username}).exec();
userId = userEntity[0]._id;
} catch (error) {
throw new NotFoundException('user not found');
}
return await this.classModel.find({user: userId}).exec();
}
Here is the test
it('get all quizes', async () => {
clientUserModel.find.mockResolvedValue('user1');
clientClassModel.find.mockResolvedValue([{title: 'test', description: 'test'}])
expect(clientUserModel.find).not.toHaveBeenCalled();
expect(clientClassModel.find).not.toHaveBeenCalled();
const result = quizesService.getAllQuizes(mockUser);
expect(clientUserModel.find).toHaveBeenCalled();
expect(clientClassModel.find).toHaveBeenCalled();
expect(result).toEqual([{title: 'test', description: 'test'}]);
})
My test is false because the assertion expect(clientClassModel.find).toHaveBeenCalled() is false
Whereas in my service I have a first call on find method of the user model, and a second call on the find method of the class model
Finally tests pass
describe("getAllQuizes", () => {
it("get all quizes, user not found", async () => {
clientUserModel.find.mockRejectedValue("user not found");
clientClassModel.find.mockResolvedValue([
{ title: "test", description: "test" },
]);
expect(clientUserModel.find).not.toHaveBeenCalled();
expect(clientClassModel.find).not.toHaveBeenCalled();
const result = quizesService.getAllQuizes(mockUser).catch((err) => {
expect(err.message).toEqual("user not found");
});
expect(clientUserModel.find).toHaveBeenCalled();
});
it("get all quizes, find quizzes", async () => {
clientUserModel.find.mockReturnValue({
_id: "1234",
username: "Test user",
});
clientClassModel.find.mockResolvedValue([
{ title: "test", description: "test" },
]);
expect(clientUserModel.find).not.toHaveBeenCalled();
expect(clientClassModel.find).not.toHaveBeenCalled();
const result = quizesService.getAllQuizes(mockUser).then((state) => {
expect(clientUserModel.find).toHaveBeenCalled();
expect(clientClassModel.find).toHaveBeenCalled();
expect(state).toEqual([{ title: "test", description: "test" }]);
});
//
});
});

How to apply Global Pipes during e2e tests

How do you apply global pipes when using Test.createTestingModule?
Normally, global pipes are added when the application is mounted in main.ts.
beforeEach(async done => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule]
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
done()
})
Here's what I do to ensure that the global pipes in my main.ts file are always in sync with my testing setup...
Start by creating a new file called main.config.ts, this should contain your global pipes, filters, etc:
import { INestApplication, ValidationPipe } from "#nestjs/common";
export function mainConfig(app: INestApplication) {
app.enableCors();
app.useGlobalPipes(new ValidationPipe());
}
Next, use the newly created mainConfig function in both the main.ts and app.e2e-spec.ts (or wherever you setup your tests):
main.ts
import { NestFactory } from "#nestjs/core";
import { mainConfig } from "main.config";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// use here
mainConfig(app);
await app.listen(3000);
}
bootstrap();
app.e2e-spec.ts
import { INestApplication } from "#nestjs/common";
import { TestingModule, Test } from "#nestjs/testing";
import { AppModule } from "app.module";
import { mainConfig } from "main.config";
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
// use here
mainConfig(app);
await app.init();
});
Now if you need to add a new pipe, you'll only have to make the change in one spot (main.config.ts) to see it reflected in both the app and the tests.
You can add them before you initialize the testing module:
beforeEach(async done => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule]
}).compile()
app = moduleFixture.createNestApplication()
// Add global pipe here
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
await app.init()
done()
})
Another approach is to declare the global pipes in the module using APP_PIPE.
#Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
Then it will be available in your e2e tests.

Resources