I have a problem with overriding provider/setup module testing in nest.js application for testing.
Module file: smsModule.ts:
import { TwilioService } from './twilio/twilio.service';
import { DynamicModule, Module } from '#nestjs/common';
import { TwilioConfig, SMS_TWILIO_CONFIG } from './twilio.config';
import { TwilioClientCustom } from './twilio/twilio-client-custom';
#Module({
imports: [],
providers: [TwilioService],
})
export class SmsModule {
static register(options: TwilioConfig): DynamicModule {
return {
module: SmsModule,
imports: [HttpModule],
providers: [
{
provide: SMS_TWILIO_CONFIG,
useValue: options,
},
TwilioService,
TwilioClientCustom,
],
exports: [TwilioService],
};
}
}
Twilio client, config files:
//client
import { TwilioConfig, SMS_TWILIO_CONFIG } from '../twilio.config';
import { Twilio } from 'twilio';
import { Inject, Injectable } from '#nestjs/common';
#Injectable()
export class TwilioClientCustom extends Twilio {
constructor(#Inject(SMS_TWILIO_CONFIG) twilioConfig: TwilioConfig) {
super(twilioConfig.accountSid, twilioConfig.authToken);
}
}
//config
import { IsString, IsNotEmpty, NotContains, IsOptional, IsArray } from 'class-validator';
const INFO = 'Must be ....';
export class TwilioConfig {
#IsString()
#IsNotEmpty()
#NotContains('OVERRIDE_WITH_', { message: INFO })
accountSid: string;
#IsString()
#IsNotEmpty()
authToken: string;
#IsArray()
#IsOptional()
#IsNotEmpty('OVERRIDE_WITH_', { message: INFO })
serviceSid: string;
}
export const SMS_TWILIO_CONFIG = 'smsTwilioConfig';
Twilio service file: twilio.service.tst:
import { HttpService } from '#nestjs/axios';
import { TwilioConfig, SMS_TWILIO_CONFIG } from '../twilio.config';
import { SendSmsTwilioService } from './../sendsms.service';
import { Inject, Injectable } from '#nestjs/common';
import { TwilioClientCustom } from './twilio-client-custom';
#Injectable()
export class TwilioService implements SendSmsTwilioService {
constructor(
#Inject(SMS_TWILIO_CONFIG) private readonly config: TwilioConfig,
private readonly client: TwilioClientCustom,
private readonly httpService: HttpService
) {}
async sendSMS(to: string, from: string, body: string): Promise<string> {
......
return this.client.messages
.create({
to, //Recipinet's number
from, //Twilio number
body, //Messages to Recipient
})
.then((message) => message.sid)
.catch(() => {
throw new Error('TWILIO accountSid or authToken not valid');
});
}
I would like to test my service:
test file:
import { Test, TestingModule } from '#nestjs/testing';
//import { TWILIO_CONFIG_SPEC } from './test.config';
import { TwilioClientCustom } from '../src/twilio/twilio-client-custom';
import { HttpService } from '#nestjs/axios';
import { TwilioConfig } from './../src/twilio.config';
import { TwilioService } from './../src/twilio/twilio.service';
import nock from 'nock';
describe('TwilioService', () => {
let service: TwilioService;
let client: TwilioClientCustom;
let httpService: HttpService;
afterEach(() => {
nock.cleanAll();
});
//const smsServiceMock = {};
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
TwilioService,
{
provide: HttpService,
useValue: {
method1: jest.fn(),
method2: jest.fn(),
method3: jest.fn(),
},
},
TwilioService,
],
imports: [
NestConfigModule.forRoot({
config: TwilioConfig,
} as Record<string, unknown>),
],
}).compile();
//getting service module from main module
httpService = moduleRef.get<HttpService>(HttpService);
client = moduleRef.get<TwilioClientCustom>(TwilioClientCustom);
service = moduleRef.get<TwilioService>(TwilioService);
});
//check service is avaible
it('Should be defined', () => {
expect(client).toBeDefined();
expect(service).toBeDefined();
expect(httpService).toBeDefined();
});
And after running test I get following errors:
Nest can't resolve dependencies of the TwilioService (?, TwilioClientCustom, HttpService). Please make sure that the argument smsTwilioConfig at index [0] is available in the RootTestModule context.
Potential solutions:
- If smsTwilioConfig is a provider, is it part of the current RootTestModule?
- If smsTwilioConfig is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing smsTwilioConfig */ ]
})
How can I solve this problem ?
smsTwilioConfig is registered with Nest's IOC via SmsModule.register(opts).
However, it seems you're attempting to test TwilioService directly with createTestingModule. Which is fine, but it does mean you need include the config with a provider or import in your test.
My guess is that you thought NestConfigModule... would do that, but that's not setting the config at the right level.
I would think the following is the right direction
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
TwilioService,
{
provide: HttpService,
useValue: {
method1: jest.fn(),
method2: jest.fn(),
method3: jest.fn(),
},
},
{
// added this
provide: SMS_TWILIO_CONFIG,
useValue: testConfig
},
],
imports: [
// removed NestConfigModule
],
}).compile();
Related
I'm experimenting with NestJs, following a tutorial. I've decided I want to write some tests on the code I've written.
I'm writing tests for the ProductController and the test file for the controller mocks the ProductModule to which the controller belongs. My ProductModule imports MongooseModule and presumably I need to mock that import in the TestingModule of the controller test file.
How can I go about mocking that import?
ProductModule;
import { Module } from '#nestjs/common';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
import { MongooseModule } from '#nestjs/mongoose';
import { ProductSchema } from './schemas/product.schema';
#Module({
imports: [
MongooseModule.forFeature([{ name: 'Product', schema: ProductSchema }]),
],
providers: [ProductService],
controllers: [ProductController],
})
export class ProductModule {}
ProductController test file;
import { Test, TestingModule } from '#nestjs/testing';
import { ProductController } from './product.controller';
import { ProductService } from './product.service';
describe('ProductController', () => {
let controller: ProductController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProductService],
controllers: [ProductController],
}).compile();
controller = module.get<ProductController>(ProductController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
For the ProductController test I'd use a custom provider like
{
provide: ProductService,
useValue: {
method1: jest.fn(),
methodN: jest.fn()
}
}
For your ProductService test I'd mock the #InjectModel('Product') by using getModelToken() and a custom provider like
{
provide: getModelToken('Product'),
useValue: {
productModelMethod1: jest.fn(),
}
}
You can see a repo here with a bunch of test examples
I'm usign nestjs-prisma in NestJs and I have in the following import for the prisma module in the app.module, so in every service I can use the PrismaService as the library allows it.
app.module.ts
import { Module } from '#nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
import { CategoryModule } from './category/category.module';
#Module({
imports: [PrismaModule.forRoot({ isGlobal: true }), CategoryModule],
})
export class AppModule {}
I want to do a e2e test just for the CategoryModule and need to mock the PrismaService, how can i do that?
You need to mook first of all the PrismaModule and then import in it the PrismaService, here is an example.
category.e2e-spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { INestApplication } from '#nestjs/common';
import * as request from 'supertest';
import { PrismaModule, PrismaService } from 'nestjs-prisma';
import { CategoryModule } from '../src/category/category.module';
describe('CategoryModule (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
// Here you have to mock the category funtions that you'll use
const mockPrismaService = {
provide: PrismaService,
useFactory: () => ({
category: {
findMany: jest.fn(() => [])
},
}),
};
// Here is the creation of the module and the definition for the service
const mockPrismaModule = {
module: PrismaModule,
providers: [mockPrismaService],
global: true,
};
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [CategoryModule, mockPrismaModule], // Here is the import for the mock module
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('(GET) /category', () => {
return request(app.getHttpServer()).get('/category').expect(200);
});
});
Hi I currently want to mock my configService injected as RegisterAs which allows me to have strong typing when accessing my environment variables, my problem with this is that when I mock the configService in my unit test and try to manipulate the environment values it doesn't I'm making it.
I show some of what I have.
My Nestjs Project:
src/
|- config/
| |- environment.ts
|- modules/
| |- auth/
| |- auth.service.ts
| |- auth.service.spec.ts
|- app.module.ts
|- main.ts
|+- ...
app.module.ts
import * as Joi from 'joi';
import { AuthModule } from './modules/auth/auth.module';
import { ConfigModule } from '#nestjs/config';
import { Module } from '#nestjs/common';
import { ScheduleModule } from '#nestjs/schedule';
import env from './config/environment';
#Module({
imports: [
ScheduleModule.forRoot(),
ConfigModule.forRoot({ // <= I load the variables and validate them with Joi
isGlobal: true,
load: [env],
validationSchema: Joi.object().keys({
JWT_SESSION: Joi.string().trim(),
JWT_SESSION_EXP: Joi.number(),
}),
}),
AuthModule,
],
})
export class AppModule {}
config/environment.ts
import { registerAs } from '#nestjs/config';
export default registerAs('env', () => ({
jwtSession: process.env.JWT_SESSION || undefined,
expSession: Number(process.env.JWT_SESSION_EXP) || undefined,
}));
auth.service.ts
import * as moment from 'moment';
import { ConfigType } from '#nestjs/config';
import { Inject, Injectable, Logger } from '#nestjs/common';
import { AuthResponse } from './interfaces/login.interface';
import { AuthenticationDto } from './dto/auth.dto';
import { HttpService } from '#nestjs/axios';
import { JwtFidelity } from './interfaces/jwt-fidelity.interface';
import env from './../../config/environment';
import jwt_decode from 'jwt-decode';
import { lastValueFrom } from 'rxjs';
#Injectable()
export class AuthService {
constructor(
#Inject(env.KEY) private configService: ConfigType<typeof env>, // <= I inject the environment variables like this
) {}
isTokenExpired(): boolean { // <= This checks if the token has expired
if (!this.configService.jwtSession) return true;
if (!this.configService.expSession) return true;
return false;
}
}
auth.service.spec.ts
Note: this is where i would like you to help me.
import { ConfigModule, ConfigService } from '#nestjs/config';
import { HttpModule, HttpService } from '#nestjs/axios';
import { Test, TestingModule } from '#nestjs/testing';
import { AuthService } from '../auth.service';
import environment from '../../../config/environment';
import { of } from 'rxjs';
const env = () => ({
auth: {
partner: {
jwtSeesion:
'jwt-token',
expSession: '1661878302',
},
},
});
describe('AuthService', () => {
let service: AuthService;
let httpService: HttpService;
let configService: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule, ConfigModule.forRoot({ load: [environment] })],
providers: [
HttpService,
AuthService,
ConfigService,
{ provide: 'CONFIGURATION(env)', useFactory: env },
],
})
.overrideProvider(HttpService)
.useValue({ request: jest.fn() })
.overrideProvider(ConfigService)
.useValue({ jwtSession: jest.fn() })
.compile();
service = module.get<AuthService>(AuthService);
httpService = module.get<HttpService>(HttpService);
configService = module.get<ConfigService>(ConfigService);
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(httpService).toBeDefined();
expect(configService).toBeDefined();
});
describe('AuthController.isTokenExpired', () => {
it('variable jwtSession undefined', async () => {
configService.jwtSession = undefined; // <= I would like the possibility of doing something like that
jest.spyOn(configService, 'jwtSession').mockReturnValue('hola' as any); // <= Or something like that
const isExpired = await service.isTokenExpired();
expect(isExpired).toBeTruthy();
});
});
});
I would like that at the level of each test I can be able to manipulate the value of the environment variables that I have injected with #Inject(env.KEY) private configService: ConfigType<typeof env>, in my auth.service.ts
I am using #nestjs/microservices for work with kafka in microservices. And ClientKafka one of them. The method "send" returning Observable, but I wanna Promise, so I used rxjs/firstValueFrom to get Promise from Observable. But when I await it, I don't get async.
import { HttpException, Inject, Injectable } from "#nestjs/common";
import { ClientKafka } from "#nestjs/microservices";
import { firstValueFrom } from "rxjs";
import { AssetGetOneDto } from "./dto/asset.get.one.dto";
import { AssetTransferDto } from "./dto/asset.transfer.dto";
#Injectable()
export class AssetService {
constructor(
#Inject("ASSET_SERVICE")
protected readonly assetClient: ClientKafka
){}
async onModuleInit() {
this.assetClient.subscribeToResponseOf('asset.get.one')
await this.assetClient.connect()
}
async onModuleDestroy() {
await this.assetClient.close();
}
async oneAsset(assetGetOneDto: AssetGetOneDto) {
const response = await firstValueFrom(this.assetClient.send('asset.get.one', assetGetOneDto))
console.log("response=",response)
if(response.status == 200){
return response.message;
}
return undefined;
}
}
Client creating:
import { Module } from "#nestjs/common";
import { ClientsModule, Transport } from "#nestjs/microservices";
import { getuid } from "process";
import { ConfigModule } from "src/config/config.moudle";
import { ConfigService } from "src/config/config.service";
import { AssetService } from "./asset.service";
#Module({
imports: [
ClientsModule.registerAsync([
{
name: "ASSET_SERVICE",
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
transport: Transport.KAFKA,
options: {
client: {
clientId: 'asset',
brokers: [configService.getBroker()]
},
consumer: {
groupId: 'asset-consumer-'+process.pid,
},
}
}),
}
])
],
providers: [AssetService],
exports: [AssetService]
})
export class AssetModule {}
I am in the process of working out a custom validator to be used to verify if a value exists in the database
I managed to get the code to work by using getConnection() from the typeorm package. But I would ideally want to inject the typeorm connection object constructor(private connection: Connection) {} this gives me more flexibility with the testing.
import { BadRequestException, Injectable } from '#nestjs/common';
import {
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { Connection, getConnection } from 'typeorm';
#ValidatorConstraint({ name: 'ExistsRule', async: true })
#Injectable()
export class ExistsRule implements ValidatorConstraintInterface {
constructor(private connection: Connection) {}
async validate(value: number | string, args: ValidationArguments) {
const { constraints } = args;
if (constraints.length === 0) {
throw new BadRequestException(`Failed validating ${value} exists.`);
}
const str = constraints[0].split(':');
const tableName = str[0];
const columnName = str[1];
const result = await getConnection().query(
`select count(*) from ${tableName} where ${columnName}=$1`,
[value],
);
// The record already exists. Failing the validation.
if (result[0].count > 0) {
return false;
}
return true;
}
defaultMessage(args: ValidationArguments) {
const { property, value } = args;
return `${property} ${value} is already taken.`;
}
}
DTO that is using this validator. (For completeness)
import {
IsEmail,
IsNotEmpty,
IsPhoneNumber,
IsString,
Validate,
} from 'class-validator';
import { ExistsRule } from 'src/customer/exists.validator';
export class CreateCustomerDto {
#IsString()
firstName: string;
#IsString()
lastName: string;
#IsEmail()
#Validate(ExistsRule, ['customer:email'])
email: string;
#IsPhoneNumber()
mobileNumber: string;
#IsNotEmpty()
password: string;
}
#Module({
imports: [
TypeOrmModule.forFeature([Customer]),
JwtModule.register({
secret: 'secret-to-be-put-in-the-env',
signOptions: { expiresIn: '300s' },
}),
CommonModule,
],
controllers: [BookingsController, AuthController],
providers: [CustomerService, AuthService, LocalStrategy, JwtStrategy],
})
export class CustomerModule {}
import { Module } from '#nestjs/common';
import { UniqueRule } from './unique.validator';
#Module({
providers: [UniqueRule],
})
export class CommonModule {}
Thanks to #JayMcDoniel's comments, and guidence. The solution was to edit main.ts and add the useContainer from class-validator module to instruct it to use the nestjs container.
import { ValidationPipe } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';
import { CommonModule } from './common/common.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
useContainer(app.select(CommonModule), { fallbackOnErrors: true });
await app.listen(3000);
}
bootstrap();