How would I mock the DataSource from TypeORM in NestJS Jest tests? - node.js

I am trying to write tests for a small project in NestJS. Here is the relevant code for context:
dummy.controller.ts
#Controller(UrlConstants.BASE_URL + 'dummy')
export class DummyContoller {
constructor(
private readonly sessionService: SessionService,
) { }
#Get('validateSession')
async checkValidateSession(#Query('sessionId') sessionId: string) {
const session = await this.sessionService.validateSession(sessionId);
console.log(session);
return { message: "OK" };
}
}
session.service.ts
#Injectable()
export class SessionService {
constructor(
private readonly sessionRepo: SessionRepository,
private readonly accountRepo: AccountRepository
) { }
#WithErrorBoundary(AuthCodes.UNKNOWN_LOGIN_ERROR)
async validateSession(sessionId: string) {
const session = await this.sessionRepo.findOneBy({ sessionId });
if (!session || this.isSessionExpired(session)) {
session && await this.sessionRepo.remove(session);
throw new HttpException({
code: AuthCodes.SESSION_TIMEOUT,
message: AuthMessages.SESSION_TIMEOUT
}, HttpStatus.UNAUTHORIZED)
}
return session;
}
...
}
session.repository.ts (Any repository)
#Injectable()
export class SessionRepository extends Repository<Session> {
constructor(private dataSource: DataSource) {
super(Session, dataSource.createEntityManager())
}
...
}
This is how I wrote my test (this is my first time writing a test using Jest and I am not really experienced in writing tests in general):
describe('DummyController', () => {
let dummyContoller: DummyContoller;
let sessionService: SessionService;
let sessionRepo: SessionRepository;
let accountRepo: AccountRepository;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [DummyContoller],
providers: [SessionService, SessionRepository, AccountRepository]
}).compile();
dummyContoller = module.get<DummyContoller>(DummyContoller);
sessionService = module.get<SessionService>(SessionService);
sessionRepo = module.get<SessionRepository>(SessionRepository);
accountRepo = module.get<AccountRepository>(AccountRepository);
})
describe('checkValidateSession', () => {
it('should return valid session', async () => {
const sessionId = "sessionId1";
const session = new Session();
jest.spyOn(sessionService, 'validateSession').mockImplementation(async (sessionId) => session);
expect(await dummyContoller.checkValidateSession(sessionId)).toBe(session);
})
})
})
Upon running the test, I encounter:
Nest can't resolve dependencies of the SessionRepository (?). Please make sure that the argument DataSource at index [0] is available in the RootTestModule context.
Potential solutions:
- If DataSource is a provider, is it part of the current RootTestModule?
- If DataSource is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing DataSource */ ]
})
I looked this problem and I came across a number of solutions but most of them had #InjectRepository() instead of creating a separate Repository class where they would provide getRepositoryToken() and then use a mock factory [Link]. I couldn't find a way to make this work.
Another solution suggested using an in-memory database solution [Link]. But this felt more like a hack rather than a solution.
How can I test the above setup?

Based on this comment, I was able to get this working by using the following in the providers in the test:
providers: [
SessionService,
{ provide: SessionRepository, useClass: SessionMockRepository },
]
SessionMockRepository contains a mocked version of all additional functions in that particular repository:
export class SessionMockRepository extends Repository<Session> {
someFunction = async () => jest.fn();
}
Currently, this works for me so I am accepting this. I am still open to more answers if there is a better way to do this.

Related

NestJS Jest error: TypeError: Cannot read properties of undefined (reading '[any variable from required config]')

While trying to cover out project in unit tests using nest's jest I've bumped into a problem of a testing module not being able to pull variables from config.
Basically, I have an EmailService, I want to test it, I use it as a Provider in my testing module. Naturally, as EmailService takes ConfigService in its constructor to pull some variables from config (that initially come from env) I put ConfigService into the providers array as well... well, then upon initialization testing module drops
NestJS Jest error: TypeError: Cannot read properties of undefined (reading 'region')
note: region variable is taken from env in a registered config module
code example of my test that throws
describe('EmailService', () => {
let emailService: EmailService;
let configService: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EmailService, ConfigService],
}).compile();
emailService = module.get<EmailService>(EmailService);
configService = module.get<ConfigService>(ConfigService);
});
it('should be defined', () => {
expect(emailService).toBeDefined();
});
});
I have came to the conclusion that it throws an error specifically because EmailService takes ConfigService in it's constructor in this way:
export class EmailService {
private readonly config: IAwsConfig;
private readonly region: IRegion;
constructor(private readonly configService: ConfigService) {
this.config = this.configService.get('aws');
this.region = this.config.region;
}
aditional info: both EmailService and ConfigService work just fine during a normal runtime, it only fails during jest testing
seems like this.configService.get method returns 'undefined' during a test run and i'm, not sure why or how to fix it. Any ideas?
In case you don't want to import the entire ConfigService but just the config values themselves, then you use them in the test as follows :)
// my-config.ts
import { registerAs } from '#nestjs/config';
export default registerAs('myConfig', () => ({ propA: 'aa', propB: 123 }));
import { Inject } from '#nestjs/common';
import { ConfigType } from '#nestjs/config';
import myConfig from './my-config.ts';
export class EmailService {
private propA: string;
private propB: number;
constructor(
#Inject(myConfig.KEY) config: ConfigType<typeof myConfig>
) {
this.propA = config.propA;
this.propB = config.propB;
}
}
import { ConfigModule, registerAs } from '#nestjs/config';
import { Test, TestingModule } from '#nestjs/testing';
describe('Test', () => {
const configValues = { propA: 'aa', proprB: 123 };
const config = registerAs('testConfig', () => configValues);
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
imports: [ConfigModule.forFeature(config)],
providers: [EmailService],
}).compile();
});
});
Was not able to find an answer for 2 hours straight, but then, 10 minutes after asking a question, there you go, an answer.
Seems like ConfigService doesn't provide configs during jest testing so you have to provide it in the testing module with replaced get method, something like such:
providers: [
EmailService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
return hardcodedConfigFromWithinTheTestFile;
}),
},
},
],

How to mock getMongoRepository in service nestjs

I write unit test for my service in nestjs. In my function delete i use getMongoRepository to delete. But i stuck in write the unit test
I've tried write the mock but it's not work
my service
async delete(systemId: string): Promise<DeleteWriteOpResultObject> {
const systemRepository = getMongoRepository(Systems);
return await systemRepository.deleteOne({ systemId });
}
my mock
import { Mock } from './mock.type';
import { Repository, getMongoRepository } from 'typeorm';
// #ts-ignore
export const mockRepositoryFactory: () => Mock<Repository<any>> = jest.fn(
() => ({
save: jest.fn(Systems => Systems),
delete: jest.fn(Systems => Systems),
deleteOne: jest.fn(Systems => Systems),
}),
);
my test
import { ExternalSystemService } from '../external-system.service';
import { Systems } from '../entities/external-system.entity';
module = await Test.createTestingModule({
providers: [
ExternalSystemService,
{
provide: getRepositoryToken(Systems),
useFactory: mockRepositoryFactory,
},
],
}).compile();
service = module.get<ExternalSystemService>(ExternalSystemService);
mockRepository = module.get(getRepositoryToken(Systems));
describe('delete', () => {
it('should delete the system', async () => {
mockRepository.delete.mockReturnValue(undefined);
const deletedSystem = await service.delete(systemOne.systemId);
expect(mockRepository.delete).toBeCalledWith({ systemId: systemOne.systemId });
expect(deletedSystem).toBe(Object);
});
I got this error
ExternalSystemService › delete › should not delete the system
ConnectionNotFoundError: Connection "default" was not found.
at new ConnectionNotFoundError (error/ConnectionNotFoundError.ts:8:9)
at ConnectionManager.Object.<anonymous>.ConnectionManager.get (connection/ConnectionManager.ts:40:19)
at Object.getMongoRepository (index.ts:300:35)
at Object.<anonymous> (external-system/tests/external-system.service.spec.ts:176:33)
at external-system/tests/external-system.service.spec.ts:7:71
at Object.<anonymous>.__awaiter (external-system/tests/external-system.service.spec.ts:3:12)
at Object.<anonymous> (external-system/tests/external-system.service.spec.ts:175:51)
You should avoid using global functions and instead use the dependency injection system; this makes testing much easier and is one of the main features of nest.
The nest typeorm module already provides a convenient way of injecting a repository:
1) Inject the repository in your service's constructor:
constructor(
#InjectRepository(Systems)
private readonly systemsRepository: MongoRepository<Systems>,
) {}
2) Use the injected repository
async delete(systemId: string): Promise<DeleteWriteOpResultObject> {
return this.systemsRepository.deleteOne({ systemId });
}
Now your mocked repository will be used in your test.

How to jest.spyOn only the base class method, not the overridden method

Trying to write test scripts for my nestjs application.
I have controller/service framework, that looks like this:
Controller:
export class MyController {
constructor(
protected _svc: MyService
) {}
#Get()
async getAll(): Promise<Array<Person>> {
return await this._svc.findAll();
}
}
Service:
#Injectable()
export class MyService extends DbService < Person > {
constructor(
private _cache: CacheService
) {
super(...);
}
async findAll() {
return super.findAll().then(res => {
res.map(s => {
this._cache.setValue(`key${s.ref}`, s);
});
return res;
});
}
Base class:
#Injectable()
export abstract class DbService<T> {
constructor() {}
async findAll(): Promise<Array<T>> {
...
}
}
My controller is the entry point when calling an endpoint on the API. This calls the service, which extends the DbService, which is what communicates with my database. There are a lot of services which all extend this DbService. In this case, the MyService class overrides the DbService "findAll" method to do some cache manipulation.
My test script has this:
let myController: MyController;
let myService: MyService;
describe("MyController", async () => {
let spy_findall, spy_cacheset;
beforeAll(() => {
this._cacheService = {
// getValue, setValue, delete methods
};
myService = new MyService(this._cacheService);
myController = new MyController(myService);
spy_findall = jest.spyOn(myService, "findAll").mockImplementation(async () => {
return [testPerson];
});
spy_cacheset = jest.spyOn(this._cacheService, "setValue");
});
beforeEach(async () => {
jest.clearAllMocks();
});
describe("getAll", () => {
it("should return an array of one person", async () => {
await myController.getAll().then(r => {
expect(r).toHaveLength(1);
expect(spy_findall).toBeCalledTimes(1);
expect(spy_cacheset).toBeCalledTimes(1);
expect(r).toEqual([testPerson]);
});
});
});
});
Now, obviously the mockImplementation of findAll mocks the "findAll" on MyService, so the test fails because spy_cacheset is never called.
What I would like to do is mock only the base method "findAll" from DbService, so that I maintain the extra functionality that exists in MyService.
Is there a way of doing this without just renaming the methods in MyService, which I would rather avoid doing?
Edited to add:
Thanks to #Jonatan lenco for such a comprehensive reponse, which I have taken on board and implemented.
I have one further question. CacheService, DbService and a whole lot of other stuff (some of which I want to mock, other that I don't) is in an external library project, "shared".
cache.service.ts
export class CacheService {...}
index.ts
export * from "./shared/cache.service"
export * from "./shared/db.service"
export * from "./shared/other.stuff"
....
This is then compiled and included as a package in node_modules.
In the project where I am writing the tests:
import { CacheService, DocumentService, OtherStuff } from "shared";
Can I still use jest.mock() for just the CacheService, without mocking the whole "shared" project?
In this case since you want to spy on an abstract class (DbService), you can spy on the prototype method:
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
Also here some recommendations for your unit tests with NestJS and Jest:
Use jest.mock() in order to simplify your mocking (in this case for CacheService). See https://jestjs.io/docs/en/es6-class-mocks#automatic-mock.
When you do jest.spyOn(), you can assert the method execution without the need of the spy object. Instead of:
spy_findall = jest.spyOn(myService, "findAll").mockImplementation(async () => {
return [testPerson];
});
...
expect(spy_findall).toBeCalledTimes(1);
You can do:
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
...
expect(DbService.prototype.findAll).toBeCalledTimes(1);
If you are mocking a class properly, you do not need to spy on the method (if you do not want to mock its implementation).
Use the Testing utilities from NestJS, it will help you a lot specially when you have complex dependency injection. See https://docs.nestjs.com/fundamentals/testing#testing-utilities.
Here is an example that applies these 4 recommendations for your unit test:
import { Test } from '#nestjs/testing';
import { CacheService } from './cache.service';
import { DbService } from './db.service';
import { MyController } from './my.controller';
import { MyService } from './my.service';
import { Person } from './person';
jest.mock('./cache.service');
describe('MyController', async () => {
let myController: MyController;
let myService: MyService;
let cacheService: CacheService;
const testPerson = new Person();
beforeAll(async () => {
const module = await Test.createTestingModule({
controllers: [MyController],
providers: [
MyService,
CacheService,
],
}).compile();
myService = module.get<MyService>(MyService);
cacheService = module.get<CacheService>(CacheService);
myController = module.get<MyController>(MyController);
jest.spyOn(DbService.prototype, 'findAll').mockImplementation(async () => {
return [testPerson];
});
});
beforeEach(async () => {
jest.clearAllMocks();
});
describe('getAll', () => {
it('Should return an array of one person', async () => {
const r = await myController.getAll();
expect(r).toHaveLength(1);
expect(DbService.prototype.findAll).toBeCalledTimes(1);
expect(cacheService.setValue).toBeCalledTimes(1);
expect(r).toEqual([testPerson]);
});
});
});
NOTE: for the testing utilities to work and also for your application to work well, you will need to add the #Controller decorator on the class MyController:
import { Controller, Get } from '#nestjs/common';
...
#Controller()
export class MyController {
...
}
About mocking specific items of another package (instead of mocking the whole package) you could do this:
Create a class in your spec file (or you can create it in another file that you import, or even in your shared module) which has a different name but has the same public method names. Note that we use jest.fn() since we do not need to provide an implementation, and that already spies in the method (no need to later do jest.spyOn() unless you have to mock the implementation).
class CacheServiceMock {
setValue = jest.fn();
}
When setting up the providers of your testing module, tell it that you are "providing" the original class but actually providing the mocked one:
const module = await Test.createTestingModule({
controllers: [MyController],
providers: [
MyService,
{ provide: CacheService, useClass: CacheServiceMock },
],
}).compile();
For more info about providers see https://angular.io/guide/dependency-injection-providers (Nest follows the same idea of Angular).

Testing mongoose models with NestJS

I'm using the mongoose module from NestJS so I have my schema and an interface, and in my service I use #InjectModel to inject my model. I do not realize how I can mock the model to inject in my service.
My service looks like this:
#Injectable()
export class AuthenticationService {
constructor(#InjectModel('User') private readonly userModel: Model<User>) {}
async createUser(dto: CreateUserDto): Promise<User> {
const model = new this.userModel(dto);
model.activationToken = this.buildActivationToken();
return await model.save();
}
}
and in my service test, I have this:
const mockMongooseTokens = [
{
provide: getModelToken('User'),
useValue: {},
},
];
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
...mockMongooseTokens,
AuthenticationService,
],
}).compile();
service = module.get<AuthenticationService>(AuthenticationService);
});
But when I run the test I got this error:
TypeError: this.userModel is not a constructor
I would also like to get my model to perform unit tests over it, as is shown in this article
I know this post is older but if anyone should get to this question again in the future here is an example of how to setup a mocked model and spy on any underlying query call methods. It took me longer than I wanted to figure this out but here is a full example test that doesn't require any extra factory functions or anything.
import { Test, TestingModule } from '#nestjs/testing';
import { getModelToken } from '#nestjs/mongoose';
import { Model } from 'mongoose';
// User is my class and UserDocument is my typescript type
// ie. export type UserDocument = User & Document; <-- Mongoose Type
import { User, UserDocument } from './models/user.model';
import { UsersRepository } from './users.repository';
import * as CustomScalars from '#common/graphql/scalars/data.scalar';
describe('UsersRepository', () => {
let mockUserModel: Model<UserDocument>;
let mockRepository: UsersRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getModelToken(User.name),
useValue: Model // <-- Use the Model Class from Mongoose
},
UsersRepository,
...Object.values(CustomScalars),
],
}).compile();
// Make sure to use the correct Document Type for the 'module.get' func
mockUserModel = module.get<Model<UserDocument>>(getModelToken(User.name));
mockRepository = module.get<UsersRepository>(UsersRepository);
});
it('should be defined', () => {
expect(mockRepository).toBeDefined();
});
it('should return a user doc', async () => {
// arrange
const user = new User();
const userID = '12345';
const spy = jest
.spyOn(mockUserModel, 'findById') // <- spy on what you want
.mockResolvedValue(user as UserDocument); // <- Set your resolved value
// act
await mockRepository.findOneById(userID);
// assert
expect(spy).toBeCalled();
});
});
Understanding mongoose Model
The error message you get is quite explicit: this.userModel is indeed not a constructor, as you provided an empty object to useValue. To ensure valid injection, useValue has to be a subclass of mongoose.Model. The mongoose github repo itself gives a consistent explanation of the underlying concept (from line 63):
* In Mongoose, the term "Model" refers to subclasses of the `mongoose.Model`
* class. You should not use the `mongoose.Model` class directly. The
* [`mongoose.model()`](./api.html#mongoose_Mongoose-model) and
* [`connection.model()`](./api.html#connection_Connection-model) functions
* create subclasses of `mongoose.Model` as shown below.
In other words, a mongoose Model is a class with several methods that attempt to connect to a database. In our case, the only Model method used is save(). Mongoose uses the javascript constructor function syntax, the same syntax can be used to write our mock.
TL;DR
The mock should be a constructor function, with a save() param.
Writing the mock
The service test is the following:
beforeEach(async () => {
function mockUserModel(dto: any) {
this.data = dto;
this.save = () => {
return this.data;
};
}
const module = await Test.createTestingModule({
providers: [
AuthenticationService,
{
provide: getModelToken('User'),
useValue: mockUserModel,
},
],
}).compile();
authenticationService = module.get<AuthenticationService>(AuthenticationService);
});
I also did a bit of refactoring, to wrap everything in the beforeEach block.
The save() implementation I chose for my tests is a simple identity function, but you can implement it differently, depending on the way you want to assert on the return value of createUser().
Limits of this solution
One problem with this solution is precisely that you assert on the return value of the function, but cannot assert on the number of calls, as save() is not a jest.fn(). I could not find a way to use module.get to access the Model Token outside of the module scope. If anyone finds a way to do it, please let me know.
Another issue is the fact that the instance of userModel has to be created within the tested class. This is problematic when you want to test findById() for example, as the model is not instantiated but the method is called on the collection. The workaround consists in adding the new keyword at the useValue level:
const module = await Test.createTestingModule({
providers: [
AuthenticationService,
{
provide: getModelToken('User'),
useValue: new mockUserModel(),
},
],
}).compile();
One more thing...
The return await syntax should not be used, as it raises a ts-lint error (rule: no-return-await). See the related github doc issue.
in response to #jbh solution, a way to resolve the problem of not instanciating the class in a call of a method like findById() is to use static methods, you can use like that
class mockModel {
constructor(public data?: any) {}
save() {
return this.data;
}
static findOne({ _id }) {
return data;
}
}
mockModel.findOne();
More info about static methods: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
// THIS IS MOCK FOR OUT TEST-APP, MODULE...
{
provide: getModelToken(User.name),
useValue: {},
},
UserService, // SUPPOSE THESE PROVIDERS ALSO NEED OUR USER-MODEL
HealthService, // SO THEY ARE SIBLINGS FOR OUT USER-MODEL
],
imports: [UserModule],
}) // SO IN THIS PLACE WE MOCK USER-MODEL AGAIN
.overrideProvider(getModelToken(User.name)) // <-----
.useValue({}) // <-----
.compile();
});
enter image description here

Nest JS unable to inject service into guard if used in module

I created basic AuthGuard, but can't inject TokenService. I am getting this error:
Error: Nest can't resolve dependencies of the AuthGuard (?). Please verify whether [0] argument is available in the current context.
app.module.ts:
#Module({
modules: [
WorkModule,
],
components: [TokenService],
})
export class ApplicationModule { }
auth.guard.ts:
#Guard()
export class AuthGuard implements CanActivate {
constructor(
private readonly tokenService: TokenService,
) { }
public canActivate(dataOrRequest, context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
work.module.ts:
#Module({
controllers: [WorkController],
components: [WorkService],
})
export class WorkModule { }
Update, work.service.ts:
import { Component, Inject, HttpStatus, HttpException } from '#nestjs/common';
const dataStore = require('nedb');
const workDB = new dataStore({ filename: '../db/work.db', autoload: true });
import * as moment from 'moment';
import { WorkDay, WorkDayDTO } from './work.model';
import { WorkHelpers } from './work.helpers';
#Component()
export class WorkService {
public async getWorkGraphic(month: number, year: number) {
return new Promise((resolve, reject) => {
// logic here
});
}
public async addOrUpdateWorkDay(day: WorkDayDTO) {
return new Promise((resolve, reject) => {
// logic here
});
}
public async removeWorkDay(workDayId: string) {
return new Promise((resolve, reject) => {
// logic here
});
}
}
But with this configuration everything is working:
#Module({
controllers: [
WorkController,
],
components: [TokenService, WorkService],
})
export class ApplicationModule { }
What exactly is causing this error and how can I get it work with 1st solution (Modules) ?
Is possible to show your TokenService and WorkerService?
You should register both always in your components to use inside all of application scope.
If you are using inside a specific module and trying to use in another module, probably you will not be able.
Another scenario. Imagine if you have A component registered in A module, B component registered in B module and imagine if ure trying to use A component inside of B module, you cant do that unless you register in Application Module or register inside A component inside B module(dont do that, only shared services should be used in all of the scopes, is just an architecture overview).

Resources