According to the documentation, we can use providers in the testing module to provide a mocked version of our repo. I ended up having this code for my test:
describe('UserController', () => {
let userController: UserController
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
UserService,
{
provide: getRepositoryToken(User),
useValue: {
//
// Trying everything here
getUsers: () => {
return Promise.resolve(['user1', 'user2'])
},
find: () => {
return Promise.resolve(['user1', 'user2'])
},
findAll: () => {
return Promise.resolve(['user1', 'user2'])
},
//
},
},
],
}).compile()
userController = app.get<UserController>(UserController)
})
describe('root', () => {
it('should return Users', () => {
expect(userController.getUsers()).toEqual(['user1', 'user2'])
})
})
})
And the controller/repository are quite straightforward:
#Controller()
export class UserController {
constructor(private readonly userService: UserService) {}
#Get()
async getUsers(): Promise<User[]> {
return this.userService.findAll()
}
#Post()
addNewUser(#Body() user: Partial<User>): Promise<number> {
return this.userService.create(user)
}
}
#Injectable()
export class UserService {
constructor(
#InjectRepository(User)
private usersRepository: Repository<User>
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find()
}
findOne(id: string): Promise<User> {
return this.usersRepository.findOne(id)
}
async remove(id: string): Promise<void> {
await this.usersRepository.delete(id)
}
async create(user: Partial<User>): Promise<number> {
return (await this.usersRepository.save(user)).id
}
}
When I run the tests, userController.getUsers() returns an empty object:
● UserController › root › should return Users
expect(received).toEqual(expected) // deep equality
Expected: ["user1", "user2"]
Received: {}
38 | describe('root', () => {
39 | it('should return Users', () => {
> 40 | expect(userController.getUsers()).toEqual(['user1', 'user2'])
| ^
41 | })
42 | })
43 | })
at Object.<anonymous> (user/user.controller.spec.ts:40:41)
What might be the issue?
The issue was quite silly - getUsers returns a promise, so I had to do something like this instead:
describe('root', () => {
it('should return Users', async () => {
const res = await userController.getUsers()
expect(res).toEqual(['user1', 'user2'])
})
})
Related
Q 1. Is it correct that the controller was created for the test?
Q 2. If i change the order of applying the filter to the module, How do i test that there will be different results?
First I applied 2 global exception filter
// global-filter.module.ts
#Module({
providers: [
{
provide: APP_FILTER,
useClass: ErrorFilter,
},
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class GlobalFilterModule {}
error-filter catch error
http-exception-filter catch httpException
This is for If it is not detected as httpException, it is for error processing.
// error.filter.ts
#Catch(Error)
export class ErrorFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost): any {
console.log('error filter');
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(HttpStatus.BAD_REQUEST).json(buildErrorResponse(exception));
}
}
// http-exception.filter.ts
#Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost): any {
console.log('http filter');
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
this.validationException(exception, response);
}
...
}
and I created exception.controller.ts for test
// exception.controller.ts
#Controller()
export class ExceptionController {
#Get('/error')
error(): void {
throw new Error();
}
#Get('/http-exception')
httpException(): void {
throw new BadRequestException();
}
}
my test
// global-filter.module.spec.ts
describe('Global Filter Module', () => {
let app: INestApplication;
let server: unknown;
beforeEach(async () => {
jest.clearAllMocks();
const testingModule: TestingModule = await Test.createTestingModule({
imports: [GlobalFilterModule],
controllers: [ExceptionController],
}).compile();
app = testingModule.createNestApplication();
server = app.getHttpServer();
await app.init();
});
afterEach(async () => {
await app.close();
});
describe('check filter order', () => {
test('should be defined', () => {
expect(app).toBeDefined();
});
test('error', () => request(server).get('/error').expect(500));
test('http-exception', () => request(server).get('/http-exception').expect(500));
});
});
I'm working on NestJs application and wrote unit test for my authenticateUser function in user.service.ts.It's has pass in my local machine.but when I deployed it in to server and run unit test, i got an error Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED.Seems like redis mock is not working.How should I mock redis and resolve this issue for working?
user.service.ts
async authenticateUser(authDto: AuthDTO): Promise<AuthResponse> {
try {
const userData = await this.userRepository.findOne({msisdn});
if(userData){
await this.redisCacheService.setCache(msisdn, userData);
}
} catch (error) {
console.log(error)
}
}
redisCache.service.ts
export class RedisCacheService {
constructor(
#Inject(CACHE_MANAGER) private readonly cache: Cache,
) {}
async setCache(key, value) {
await this.cache.set(key, value);
}
}
user.service.spec.ts
describe('Test User Service', () => {
let userRepository: Repository<UserEntity>;
let userService: UserService;
let redisCacheService: RedisCacheService;
let cacheManager: any;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
UserEntity,
RedisCacheService,
{
provide: getRepositoryToken(UserEntity),
useClass: registeredApplicationRepositoryMockFactory,
},
],
imports: [CacheModule.register({})],
}).compile();
userService = module.get<UserService>(UserService);
userRepository = module.get<Repository<UserEntity>>(
getRepositoryToken(UserEntity),
);
redisCacheService = module.get<RedisCacheService>(RedisCacheService);
cacheManager = module.get<any>(CACHE_MANAGER);
});
it('authenticateUser should return success response', async () => {
const userEntity = { id: 1, name: 'abc', age: 25 };
const mockSuccessResponse = new AuthResponse(
HttpStatus.OK,
STRING.SUCCESS,
`${STRING.USER} ${STRING.AUTHENTICATE} ${STRING.SUCCESS}`,
{},
);
jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(userEntity);
jest.spyOn(redisCacheService, 'setCache').mockResolvedValueOnce(null);
expect(await userService.authenticateUser(mockAuthBody)).toEqual(mockSuccessResponse);
});
});
You can mock CACHE_MANAGER using a custom provider:
import { CACHE_MANAGER } from '#nestjs/common';
import { Cache } from 'cache-manager';
describe('AppService', () => {
let service: AppService;
let cache: Cache;
beforeEach(async () => {
const app = await Test.createTestingModule({
providers: [
AppService,
{
provide: CACHE_MANAGER,
useValue: {
get: () => 'any value',
set: () => jest.fn(),
},
},
],
})
.compile();
service = app.get<AppService>(AppService);
cache = app.get(CACHE_MANAGER);
});
// Then you can use jest.spyOn() to spy and mock
it(`should cache the value`, async () => {
const spy = jest.spyOn(cache, 'set');
await service.cacheSomething();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toEqual('key');
expect(spy.mock.calls[0][1]).toEqual('value');
});
it(`should get the value from cache`, async () => {
const spy = jest.spyOn(cache, 'get');
await service.getSomething();
expect(spy).toHaveBeenCalledTimes(1);
});
it(`should return the value from the cache`, async () => {
jest.spyOn(cache, 'get').mockResolvedValueOnce('value');
const res = await service.getSomething();
expect(res).toEqual('value');
}),
});
More details on Custom Providers: https://docs.nestjs.com/fundamentals/custom-providers
Two more things, for unit testing you shouldn't import modules but mock the dependencies instead. And as Daniel said, UserService is not using CACHE_MANAGER but RedisCacheService, so you should mock RedisCacheService.
Usually the best thing to do is to only provide the service you're testing and mock the dependencies.
in order to use the jest spy functions you need to return the jest function directly.
providers: [
AppService,
{
provide: CACHE_MANAGER,
useValue: {
get: () => 'any value',
set: jest.fn(),
},
},
],
I've the following code and I need to mock the connection.execute() function as this belongs to the third-party lib snowflake-sdk and is't not part of my unit test:
import * as SnowflakeSDK from 'snowflake-sdk';
import { Injectable } from '#nestjs/common';
#Injectable()
export class SnowflakeClient {
export(bucket: string, filename: string, query: string) {
return new Promise((resolve, reject) => {
const connection = this.getConnection();
const command = `COPY INTO '${bucket}${filename}' FROM (${query})`;
connection.execute({
sqlText: command,
complete: function (err) {
try {
if (err) {
return reject(err);
} else {
return resolve(true);
}
} finally {
connection.destroy(function (err) {
if (err) {
console.error('Unable to disconnect: ' + err.message);
}
});
}
},
});
});
}
getConnection() {
const connection = SnowflakeSDK.createConnection({
account: process.env.SNOWFLAKE_HOST!,
username: process.env.SNOWFLAKE_USERNAME!,
password: process.env.SNOWFLAKE_PASSWORD!,
role: process.env.SNOWFLAKE_ROLE!,
warehouse: process.env.SNOWFLAKE_WAREHOUSE!,
database: process.env.SNOWFLAKE_DATABASE!,
schema: process.env.SNOWFLAKE_SCHEMA!,
});
connection.connect(function (err: Error): void {
if (err) {
throw err;
}
});
return connection;
}
}
So I've created the following unit test:
import { SnowflakeClient } from 'src/snowflake/snowflake-client';
import { Test } from '#nestjs/testing';
describe('SnowflakeClient', () => {
let snowflakeClient: SnowflakeClient;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [SnowflakeClient],
}).compile();
snowflakeClient = moduleRef.get<SnowflakeClient>(SnowflakeClient);
});
describe('export', () => {
it('should export a SQL query', async () => {
const connectionMock = jest.fn().mockImplementation(() => ({
execute: function (bucket: string, filename: string, sql: string) {
console.log(`${bucket}${filename}: ${sql}`);
},
}));
jest.spyOn(snowflakeClient, 'getConnection').mockImplementation(connectionMock);
const bucket = 's3://bucketName';
const filename = '/reports/test.csv';
const sql = 'select * from customers limit 100';
await expect(await snowflakeClient.export(bucket, filename, sql)).resolves.not.toThrow();
}, 10000);
});
});
But the connectionMock.execute isn't being called correctly as I'm getting the following error:
FAIL src/snowflake/snowflake-client.spec.ts (13.518 s)
SnowflakeClient
export
✕ should export a SQL query (10018 ms)
● SnowflakeClient › export › should export a SQL query
thrown: "Exceeded timeout of 10000 ms for a test.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
14 |
15 | describe('export', () => {
> 16 | it('should export a SQL query', async () => {
| ^
17 | const connectionMock = jest.fn().mockImplementation(() => ({
18 | execute: function (bucket: string, filename: string, sql: string) {
19 | console.log(`${bucket}${filename}: ${sql}`);
at src/snowflake/snowflake-client.spec.ts:16:5
at src/snowflake/snowflake-client.spec.ts:15:3
at Object.<anonymous> (src/snowflake/snowflake-client.spec.ts:4:1)
I would like to test the SnowflakeClient.export() method but I need to mock the snowflake-sdk module as it isn't part of my code.
Anybody knows what I'm doing wrong?
When using sdk, it's better to pass them through the injection system of NestJs with a custom provider:
//////////////////////////////////
// 1. Provide the sdk in the module.
//////////////////////////////////
const SnowflakeConnectionProvider = {
provide: 'SNOWFLAKE_CONNECTION',
useFactory: () => {
const connection = SnowflakeSDK.createConnection({
account: process.env.SNOWFLAKE_HOST!,
username: process.env.SNOWFLAKE_USERNAME!,
password: process.env.SNOWFLAKE_PASSWORD!,
role: process.env.SNOWFLAKE_ROLE!,
warehouse: process.env.SNOWFLAKE_WAREHOUSE!,
database: process.env.SNOWFLAKE_DATABASE!,
schema: process.env.SNOWFLAKE_SCHEMA!,
});
connection.connect(function (err: Error): void {
if (err) {
throw err;
}
});
return connection;
},
inject: [],
}
#Module({
providers: [
SnowflakeClient,
SnowflakeConnectionProvider,
]
})
export class SnowflakeModule {}
//////////////////////////////////
// 2. Inject the connection into your `SnowflakeClient`
//////////////////////////////////
#Injectable()
export class SnowflakeClient {
constructor(#Inject('SNOWFLAKE_CONNECTION') private readonly connection: SnowflakeConnection) {}
// ...
}
//////////////////////////////////
// 3. Mock the dependency in tests
//////////////////////////////////
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [
{
provide: 'SNOWFLAKE_CONNECTION',
useValue: { /* Mock */},
},
SnowflakeClient,
],
}).compile();
snowflakeClient = moduleRef.get<SnowflakeClient>(SnowflakeClient);
});
You will have a much better control over the tests.
So I got this fixed doing this (but I'm not sure it's the best way):
const connectionMock = jest.createMockFromModule<Connection>('snowflake-sdk');
connectionMock.execute = jest.fn().mockImplementation(() => {
return Promise.resolve(null);
});
jest.spyOn(snowflakeClient, 'getConnection').mockImplementation(() => {
return connectionMock;
});
expect(snowflakeClient.export(bucket, filename, sql)).resolves.not.toThrow();
Let me know your thoughts :)
This is my nodejs typescript class and written jest unit test for isHealthy() public method.
Test coverage shows that this.pingCheck() then block, catch and last return statement are not covered.
Please advise.
Can we do unit test for pingCheck private method ?
This my class
import { HttpService, Injectable } from '#nestjs/common';
import { DependencyUtlilizationService } from '../dependency-utlilization/dependency-utlilization.service';
import { ComponentType } from '../enums/component-type.enum';
import { HealthStatus } from '../enums/health-status.enum';
import { ComponentHealthCheckResult } from '../interfaces/component-health-check-result.interface';
import { ApiHealthCheckOptions } from './interfaces/api-health-check-options.interface';
#Injectable()
export class ApiHealthIndicator {
private healthIndicatorResponse: {
[key: string]: ComponentHealthCheckResult;
};
constructor(
private readonly httpService: HttpService,
private readonly dependencyUtilizationService: DependencyUtlilizationService,
) {
this.healthIndicatorResponse = {};
}
private async pingCheck(api: ApiHealthCheckOptions): Promise<boolean> {
let result = this.dependencyUtilizationService.isRecentlyUsed(api.key);
if (result) {
await this.httpService.request({ url: api.url }).subscribe(() => {
return true;
});
}
return false;
}
async isHealthy(
listOfAPIs: ApiHealthCheckOptions[],
): Promise<{ [key: string]: ComponentHealthCheckResult }> {
for (const api of listOfAPIs) {
const apiHealthStatus = {
status: HealthStatus.fail,
type: ComponentType.url,
componentId: api.key,
description: `Health Status of ${api.url} is: fail`,
time: Date.now(),
output: '',
links: {},
};
await this.pingCheck(api)
.then(response => {
apiHealthStatus.status = HealthStatus.pass;
apiHealthStatus.description = `Health Status of ${api.url} is: pass`;
this.healthIndicatorResponse[api.key] = apiHealthStatus;
})
.catch(rejected => {
this.healthIndicatorResponse[api.key] = apiHealthStatus;
});
}
return this.healthIndicatorResponse;
}
}
This is my unit test code.
I get the following error when I run npm run test
(node:7876) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'status' of undefined
(node:7876) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 6)
import { HttpService } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { DependencyUtlilizationService } from '../dependency-utlilization/dependency-utlilization.service';
import { ApiHealthIndicator } from './api-health-indicator';
import { ApiHealthCheckOptions } from './interfaces/api-health-check-options.interface';
import { HealthStatus } from '../enums/health-status.enum';
describe('ApiHealthIndicator', () => {
let apiHealthIndicator: ApiHealthIndicator;
let httpService: HttpService;
let dependencyUtlilizationService: DependencyUtlilizationService;
let dnsList: [{ key: 'domain_api'; url: 'http://localhost:3001' }];
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiHealthIndicator,
{
provide: HttpService,
useValue: new HttpService(),
},
{
provide: DependencyUtlilizationService,
useValue: new DependencyUtlilizationService(),
},
],
}).compile();
apiHealthIndicator = module.get<ApiHealthIndicator>(ApiHealthIndicator);
httpService = module.get<HttpService>(HttpService);
dependencyUtlilizationService = module.get<DependencyUtlilizationService>(
DependencyUtlilizationService,
);
});
it('should be defined', () => {
expect(apiHealthIndicator).toBeDefined();
});
it('isHealthy should return status as true when pingCheck return true', () => {
jest
.spyOn(dependencyUtlilizationService, 'isRecentlyUsed')
.mockReturnValue(true);
const result = apiHealthIndicator.isHealthy(dnsList);
result.then(response =>
expect(response['domain_api'].status).toBe(HealthStatus.pass),
);
});
it('isHealthy should return status as false when pingCheck return false', () => {
jest
.spyOn(dependencyUtlilizationService, 'isRecentlyUsed')
.mockReturnValue(false);
jest.spyOn(httpService, 'request').mockImplementation(config => {
throw new Error('could not call api');
});
const result = apiHealthIndicator.isHealthy(dnsList);
result
.then(response => {
expect(response['domain_api'].status).toBe(HealthStatus.fail);
})
.catch(reject => {
expect(reject['domain_api'].status).toBe(HealthStatus.fail);
});
});
});
Looks like you should define the status before initialize the unit test, try to grab some more logs using console.log and for the second test, added catch block to make sure you're grabing the failures
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
});