I want to get the MongoConnection before my tests and remove all collections after the test.
I create a file like test-setup.js:
const mongoose = require('mongoose')
mongoose.promise = global.Promise
async function removeAllCollections () {
const collections = Object.keys(mongoose.connection.collections)
for (const collectionName of collections) {
const collection = mongoose.connection.collections[collectionName]
await collection.deleteMany()
}
}
async function dropAllCollections () {
const collections = Object.keys(mongoose.connection.collections)
for (const collectionName of collections) {
const collection = mongoose.connection.collections[collectionName]
console.log(collectionName)
try {
await collection.drop()
} catch (error) {
// Sometimes this error happens, but you can safely ignore it
if (error.message === 'ns not found') return
// This error occurs when you use it.todo. You can
// safely ignore this error too
if (error.message.includes('a background operation is currently running')) return
console.log(error.message)
}
}
}
module.exports = {
setupDB (databaseName, runSaveMiddleware = false) {
// Connect to Mongoose
beforeAll(async () => {
const url = 'mongodb://127.0.0.1:27017/myDb'
let conn = await mongoose.connect(url, { useNewUrlParser: true })
console.log(conn.collections)
})
// Seeds database before each test
// beforeEach(async () => {
// await seedDatabase(runSaveMiddleware)
// })
// Cleans up database between each test
afterEach(async () => {
await removeAllCollections()
})
// Disconnect Mongoose
afterAll(async () => {
await dropAllCollections()
await mongoose.connection.close()
})
}
}
At the moment the seedDatabase is commented, but I'd like to create a database per each e2e files, but it's a second step.
So, I add this file to jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFilesAfterEnv": ["<rootDir>/test-setup.js"]
}
And on my test:
import * as request from 'supertest';
import {
APP_URL,
TESTER_EMAIL,
TESTER_PASSWORD,
MAIL_HOST,
MAIL_PORT,
} from '../utils/constants';
describe('Auth user (e2e)', () => {
const app = APP_URL;
const mail = `http://${MAIL_HOST}:${MAIL_PORT}`;
const newUserFirstName = `Tester${Date.now()}`;
const newUsername = `E2E.${Date.now()}`;
const newUserEmail = `User.${Date.now()}#example.com`;
const newUserPassword = `secret`;
const { setupDB } = require('../test-setup.js')
setupDB('users', true)
it('Login: /api/v1/auth/email/login (POST)', () => {
return request(app)
.post('/api/v1/auth/email/login')
.send({ email: TESTER_EMAIL, password: TESTER_PASSWORD })
.expect(404)
.expect(({ body }) => {
expect(404);
expect(body.error).toBe('Not Found');
});
});
it('Register new user: /api/v1/auth/email/register (POST)', async () => {
return request(app)
.post('/api/v1/auth/email/register')
.send({
email: newUserEmail,
password: newUserPassword,
name: newUserFirstName,
username: newUsername,
})
.expect(201);
});
The test works, but still using the "main" database, the same database that I can find on app.module
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
uri: configService.get('database.url'),
}),
inject: [ConfigService],
}),
How can use the new database per each tests and delete the collections after the tests?
If I put console.log on my test-setup.js I can see the output.
UPDATE:
app.module.ts
import { Module } from '#nestjs/common';
import { ConfigModule, ConfigService } from '#nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { MongooseModule } from '#nestjs/mongoose';
import { AuthModule } from './auth/auth.module';
import { MailModule } from './mail/mail.module';
import { I18nModule } from 'nestjs-i18n/dist/i18n.module';
import { I18nJsonParser } from 'nestjs-i18n/dist/parsers/i18n.json.parser';
import { HeaderResolver } from 'nestjs-i18n';
import { MailerModule } from '#nestjs-modules/mailer';
import { MailConfigService } from './mail/mail-config.service';
import databaseConfig from './config/database.config';
import authConfig from './config/auth.config';
import appConfig from './config/app.config';
import mailConfig from './config/mail.config';
import * as path from 'path';
import { ForgotModule } from './forgot/forgot.module';
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env', '.env.dev', '.env.stage', '.env.prod'], //if a variable is found in multiple files, the first one takes precedence.
load: [
databaseConfig,
authConfig,
appConfig,
mailConfig,
],
}),
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
uri: configService.get('database.url'),
// useNewUrlParser: true,
// useFindAndModify: false,
// useCreateIndex: true,
}),
inject: [ConfigService],
}),
MailerModule.forRootAsync({
useClass: MailConfigService,
}),
I18nModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
fallbackLanguage: configService.get('app.fallbackLanguage'),
parserOptions: {
path: path.join(
configService.get('app.workingDirectory'),
'src',
'i18n',
'translations',
),
},
}),
parser: I18nJsonParser,
inject: [ConfigService],
resolvers: [new HeaderResolver(['x-custom-lang'])],
}),
AuthModule,
UsersModule,
ForgotModule,
MailModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Related
I have an AppModule file as follows:
import { Module } from '#nestjs/common'
import { RabbitMQModule } from '#golevelup/nestjs-rabbitmq'
#Module({
imports: [
RabbitMQModule.forRoot(RabbitMQModule, {
exchanges: [
{
name: 'my_rabbit',
type: 'direct',
},
],
uri: process.env.RABBITMQ_URI,
connectionInitOptions: { wait: true },
}),
],
})
export class AppModule {}
I have tried to mock rabbitmq using #golevelup/nestjs-rabbitmq like this:
import { Module } from '#nestjs/common'
import { RabbitMQModule } from '#golevelup/nestjs-rabbitmq'
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
AppModule
],
})
.overrideProvider(AmqpConnection)
.useValue(createMock<AmqpConnection>())
.compile()
})
This is giving me error:
[Nest] 2745 - 24/07/2022, 17:02:54 ERROR [AmqpConnection] Disconnected from RabbitMQ broker (default)
Error: connect ECONNREFUSED 127.0.0.1:5672
If i mock the whole rabbitmq module like:
jest.mock('#golevelup/nestjs-rabbitmq')
I will get errors like:
Nest cannot create the AppModule instance.
The module at index [0] of the AppModule "imports" array is undefined.
Has anyone successfully mocked RabbitMQ? Please assist if possible.
I solve this problem mocking an AmqpConnection like this.
import { AmqpConnection } from "#nestjs-plus/rabbitmq";
import { TestingModule, Test } from "#nestjs/testing";
import { IntegrationQueueService } from "./integration-queue.service";
describe('IntegrationQueueService', () => {
type MockType<T> = {
[P in keyof T]?: jest.Mock<{}>;
};
const mockFactory: () => MockType<AmqpConnection> = jest.fn(() => ({
publish: jest.fn(() => AmqpConnection),
}))
let service: IntegrationQueueService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
IntegrationQueueService,
{
provide: AmqpConnection,
useFactory: mockFactory,
},
],
})
.compile();
service = module.get<IntegrationQueueService> (IntegrationQueueService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
})
I use the NestJS framework and typeorm. When working with a database, all data is successfully saved. There are no problems with the connection. I try to configure e2e test with Jest. Unfortunately, I got 2 errors:
TypeError: Cannot read property 'getHttpServer' of undefined
and
No repository for "User" was found. Looks like this entity is not registered in current "default" connection?
I tried to setup test env by using this tutorial.
My files:
app.e2e-spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { INestApplication } from '#nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { TypeOrmModule } from '#nestjs/typeorm';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
TypeOrmModule.forRoot({
'type': 'mssql',
'host': 'localhost',
'port': 1433,
'username': 'gift_draw_db',
'password': '',
'database': 'gift_draw_local',
'entities': ['./**/*.entity.ts'],
'synchronize': false,
}),
],
providers: [],
}).compile();
app = moduleFixture.createNestApplication();
console.error(process.env.DB_DATABASE_NAME, '<------------------------------'); // NEVER SEEN THIS
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/api/ (GET)', async () => {
return request(app.getHttpServer()) // 1st error
.get('/api/')
.expect(200)
.expect('Working!');
});
});
app.module.ts
#Module({
imports: [
ConfigModule,
AuthModule,
DrawModule,
UserModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
user.e2e-spec.ts
describe('User', () => {
let app: INestApplication;
let repository: Repository<User>;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [
UserModule,
TypeOrmModule.forRoot({
'type': 'mssql',
'host': 'localhost',
'port': 1433,
'username': 'gift_draw_db',
'password': '',
'database': 'gift_draw_local',
'entities': ['./**/*.entity.ts'],
'synchronize': false,
}),
],
}).compile();
app = module.createNestApplication();
repository = module.get('UserRepository');
await app.init();
});
afterEach(async () => {
await repository.query(`DELETE FROM users;`);
});
afterAll(async () => {
await app.close();
});
});
user.module.ts
#Module({
controllers: [UserController],
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
exports: [UserService, TypeOrmModule],
})
export class UserModule {
}
config.module.ts
#Module({
imports: [
NestConfigModule.forRoot({
isGlobal: true,
load: [configuration]
}),
TypeOrmModule.forRoot(process.env.NODE_ENV === 'production' ? {...configProd} : {...configDev} )
],
})
export class ConfigModule {
}
ConfigModule has the same credentials as testing one.
The problem is that when you start the app the base dir is "dist" while when you start it with for the "e2e" tests, the base dir is "src".
You can change the entities definition to something similar to this:
entities: [__dirname + '/../**/*.entity.ts']
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" }]);
});
//
});
});
Working on a project with Nestjs 6.x, Mongoose, Mongo, etc...
Regarding to the Back End, in my use case, I must change the connection of one of my databases depending of some conditions/parameters coming from some requests.
Basically, I have this
mongoose.createConnection('mongodb://127.0.0.1/whatever-a', { useNewUrlParser: true })
and I want to change to, for example
mongoose.createConnection('mongodb://127.0.0.1/whatever-b', { useNewUrlParser: true })
Therefore, I have in Nestjs the first provider
export const databaseProviders = [
{
provide: 'DbConnectionToken',
useFactory: async (): Promise<typeof mongoose> =>
await mongoose.createConnection('mongodb://127.0.0.1/whatever', { useNewUrlParser: true })
}
I was researching for a while and I found out that in release Nestjs 6.x there are provider requests allowing me to modify dynamically Per-request the injection of some providers.
Anyway, I don't know how to achieve my change neither if it is going to be working in case I'd achieve that
Can anyone help or guide me?
Many thanks in advance.
You can do the following using Nest's built-in Mongoose package:
/*************************
* mognoose.service.ts
*************************/
import { Inject, Injectable, Scope } from '#nestjs/common';
import { MongooseOptionsFactory, MongooseModuleOptions } from '#nestjs/mongoose';
import { REQUEST } from '#nestjs/core';
import { Request } from '#nestjs/common';
#Injectable({ scope: Scope.REQUEST })
export class MongooseConfigService implements MongooseOptionsFactory {
constructor(
#Inject(REQUEST) private readonly request: Request,) {
}
createMongooseOptions(): MongooseModuleOptions {
return {
uri: request.params.uri, // Change this to whatever you want; you have full access to the request object.
};
}
}
/*************************
* mongoose.module.ts
*************************/
import { Module } from '#nestjs/common';
import { MongooseModule } from '#nestjs/mongoose';
import { MongooseConfigService } from 'mognoose.service';
#Module({
imports: [
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
}),
]
})
export class DbModule {}
Then, you can attach whatever you want to the request and change the database per request; hence the use of the Scope.REQUEST. You can read more about Injection Scopes on their docs.
Edit: If you run into issues with PassportJS (or any other package) or the request is empty, it seems to be an error that relates to PassportJS (or the other package) not supporting request scopes; you may read more about the issue on GitHub regarding PassportJS.
I did a simple implementation for nest-mongodb,
The main changes are in mongo-core.module.ts where I store the connections in a map and used them if available instead of creating a new connection every time.
import {
Module,
Inject,
Global,
DynamicModule,
Provider,
OnModuleDestroy,
} from '#nestjs/common';
import { ModuleRef } from '#nestjs/core';
import { MongoClient, MongoClientOptions } from 'mongodb';
import {
DEFAULT_MONGO_CLIENT_OPTIONS,
MONGO_MODULE_OPTIONS,
DEFAULT_MONGO_CONTAINER_NAME,
MONGO_CONTAINER_NAME,
} from './mongo.constants';
import {
MongoModuleAsyncOptions,
MongoOptionsFactory,
MongoModuleOptions,
} from './interfaces';
import { getClientToken, getContainerToken, getDbToken } from './mongo.util';
import * as hash from 'object-hash';
#Global()
#Module({})
export class MongoCoreModule implements OnModuleDestroy {
constructor(
#Inject(MONGO_CONTAINER_NAME) private readonly containerName: string,
private readonly moduleRef: ModuleRef,
) {}
static forRoot(
uri: string,
dbName: string,
clientOptions: MongoClientOptions = DEFAULT_MONGO_CLIENT_OPTIONS,
containerName: string = DEFAULT_MONGO_CONTAINER_NAME,
): DynamicModule {
const containerNameProvider = {
provide: MONGO_CONTAINER_NAME,
useValue: containerName,
};
const connectionContainerProvider = {
provide: getContainerToken(containerName),
useFactory: () => new Map<any, MongoClient>(),
};
const clientProvider = {
provide: getClientToken(containerName),
useFactory: async (connections: Map<any, MongoClient>) => {
const key = hash.sha1({
uri: uri,
clientOptions: clientOptions,
});
if (connections.has(key)) {
return connections.get(key);
}
const client = new MongoClient(uri, clientOptions);
connections.set(key, client);
return await client.connect();
},
inject: [getContainerToken(containerName)],
};
const dbProvider = {
provide: getDbToken(containerName),
useFactory: (client: MongoClient) => client.db(dbName),
inject: [getClientToken(containerName)],
};
return {
module: MongoCoreModule,
providers: [
containerNameProvider,
connectionContainerProvider,
clientProvider,
dbProvider,
],
exports: [clientProvider, dbProvider],
};
}
static forRootAsync(options: MongoModuleAsyncOptions): DynamicModule {
const mongoContainerName =
options.containerName || DEFAULT_MONGO_CONTAINER_NAME;
const containerNameProvider = {
provide: MONGO_CONTAINER_NAME,
useValue: mongoContainerName,
};
const connectionContainerProvider = {
provide: getContainerToken(mongoContainerName),
useFactory: () => new Map<any, MongoClient>(),
};
const clientProvider = {
provide: getClientToken(mongoContainerName),
useFactory: async (
connections: Map<any, MongoClient>,
mongoModuleOptions: MongoModuleOptions,
) => {
const { uri, clientOptions } = mongoModuleOptions;
const key = hash.sha1({
uri: uri,
clientOptions: clientOptions,
});
if (connections.has(key)) {
return connections.get(key);
}
const client = new MongoClient(
uri,
clientOptions || DEFAULT_MONGO_CLIENT_OPTIONS,
);
connections.set(key, client);
return await client.connect();
},
inject: [getContainerToken(mongoContainerName), MONGO_MODULE_OPTIONS],
};
const dbProvider = {
provide: getDbToken(mongoContainerName),
useFactory: (
mongoModuleOptions: MongoModuleOptions,
client: MongoClient,
) => client.db(mongoModuleOptions.dbName),
inject: [MONGO_MODULE_OPTIONS, getClientToken(mongoContainerName)],
};
const asyncProviders = this.createAsyncProviders(options);
return {
module: MongoCoreModule,
imports: options.imports,
providers: [
...asyncProviders,
clientProvider,
dbProvider,
containerNameProvider,
connectionContainerProvider,
],
exports: [clientProvider, dbProvider],
};
}
async onModuleDestroy() {
const clientsMap: Map<any, MongoClient> = this.moduleRef.get<
Map<any, MongoClient>
>(getContainerToken(this.containerName));
if (clientsMap) {
await Promise.all(
[...clientsMap.values()].map(connection => connection.close()),
);
}
}
private static createAsyncProviders(
options: MongoModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
} else if (options.useClass) {
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
} else {
return [];
}
}
private static createAsyncOptionsProvider(
options: MongoModuleAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: MONGO_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
} else if (options.useExisting) {
return {
provide: MONGO_MODULE_OPTIONS,
useFactory: async (optionsFactory: MongoOptionsFactory) =>
await optionsFactory.createMongoOptions(),
inject: [options.useExisting],
};
} else if (options.useClass) {
return {
provide: MONGO_MODULE_OPTIONS,
useFactory: async (optionsFactory: MongoOptionsFactory) =>
await optionsFactory.createMongoOptions(),
inject: [options.useClass],
};
} else {
throw new Error('Invalid MongoModule options');
}
}
}
Check out the full implementation
I am getting issues while unit testing my controller and getting an error "Nest can't resolve dependencies of my service".
For maximum coverage I wanted to unit test controller and respective services and would like to mock external dependencies like mongoose connection. For the same I already tried suggestions mentioned in the below link but didn't find any luck with that:
https://github.com/nestjs/nest/issues/194#issuecomment-342219043
Please find my code below:
export const deviceProviders = [
{
provide: 'devices',
useFactory: (connection: Connection) => connection.model('devices', DeviceSchema),
inject: ['DbConnectionToken'],
},
];
export class DeviceService extends BaseService {
constructor(#InjectModel('devices') private readonly _deviceModel: Model<Device>) {
super();
}
async getDevices(group): Promise<any> {
try {
return await this._deviceModel.find({ Group: group }).exec();
} catch (error) {
return Promise.reject(error);
}
}
}
#Controller()
export class DeviceController {
constructor(private readonly deviceService: DeviceService) {
}
#Get(':group')
async getDevices(#Res() response, #Param('group') group): Promise<any> {
try {
const result = await this.deviceService.getDevices(group);
return response.send(result);
}
catch (err) {
return response.status(422).send(err);
}
}
}
#Module({
imports: [MongooseModule.forFeature([{ name: 'devices', schema: DeviceSchema }])],
controllers: [DeviceController],
components: [DeviceService, ...deviceProviders],
})
export class DeviceModule { }
Unit test:
describe('DeviceController', () => {
let deviceController: DeviceController;
let deviceService: DeviceService;
const response = {
send: (body?: any) => { },
status: (code: number) => response,
};
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [DeviceController],
components: [DeviceService, ...deviceProviders],
}).compile();
deviceService = module.get<DeviceService>(DeviceService);
deviceController = module.get<DeviceController>(DeviceController);
});
describe('getDevices()', () => {
it('should return an array of devices', async () => {
const result = [{
Group: 'group_abc',
DeviceId: 'device_abc',
},
{
Group: 'group_xyz',
DeviceId: 'device_xyz',
}];
jest.spyOn(deviceService, 'getDevices').mockImplementation(() => result);
expect(await deviceController.getDevices(response, null)).toBe(result);
});
});
});
When I am running my test case above, I am getting two errors:
Nest can't resolve dependencies of the DeviceService (?). Please make sure that the argument at index [0] is available in the current context.
Cannot spyOn on a primitive value; undefined given
Example code:
import { Test } from '#nestjs/testing';
import { getModelToken } from '#nestjs/mongoose';
describe('auth', () => {
let deviceController: DeviceController;
let deviceService: DeviceService;
const mockRepository = {
find() {
return {};
}
};
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [DeviceModule]
})
.overrideProvider(getModelToken('Auth'))
.useValue(mockRepository)
.compile();
deviceService = module.get<DeviceService>(DeviceService);
});
// ...
});
You are not injecting the correct token here. Instead of a plain string you have to use the function getModelToken.
import { getModelToken } from '#nestjs/mongoose';
// ...
{ provide: getModelToken('devices'), useFactory: myFactory },
Here is the solution provided by this repo. See the mongo-sample. I am testing my API using the #injectModel and another service. Here's the snippet:
import { CategoriesService } from './../categories/categories.service';
import { getModelToken } from '#nestjs/mongoose';
import { Test, TestingModule } from '#nestjs/testing';
import { ProductsService } from './products.service';
describe('ProductsService', () => {
let service: ProductsService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
// getModelToken to mock the MongoDB connection
providers: [
ProductsService,
CategoriesService,
{
provide: getModelToken('Product'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
findByIdAndUpdate: jest.fn(),
findByIdAndRemove: jest.fn(),
save: jest.fn(),
},
},
{
provide: getModelToken('Category'),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
findByIdAndUpdate: jest.fn(),
findByIdAndRemove: jest.fn(),
save: jest.fn(),
},
},
],
}).compile();
service = module.get<ProductsService>(ProductsService);
});
// your test case
});