I made unit test for sendFCM in push.service.spec.ts. but it's not working on the nest server.
Issue #1 : How can I normally give the return value of sendmulticast method?
Property 'success' does not exist on type 'void' below 'success' code.
```expect((await response).success).toBe(true);```
Issue #2 : I looked at many similar issue solutions, but couldn't find a solution to the below error. How can I test normally?
Nest can't resolve dependencies of the PushService (FIREBASE_ADMIN_INJECT, ?). Please make sure that the argument HttpService at index [1] is available in the RootTestModule
[Error Message]
FAIL push/push.service.spec.ts (24.852 s)
● PushService › should be defined
Nest can't resolve dependencies of the PushService (FIREBASE_ADMIN_INJECT, ?). Please make sure that the argument HttpService at index [1] is available in the RootTestModule context.
Potential solutions:
- If HttpService is a provider, is it part of the current RootTestModule?
- If HttpService is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing HttpService */ ]
})
at TestingInjector.lookupComponentInParentModules (../node_modules/#nestjs/core/injector/injector.js:231:19)
at TestingInjector.resolveComponentInstance (../node_modules/#nestjs/core/injector/injector.js:184:33)
at TestingInjector.resolveComponentInstance (../node_modules/#nestjs/testing/testing-injector.js:16:45)
at resolveParam (../node_modules/#nestjs/core/injector/injector.js:106:38)
at async Promise.all (index 1)
at TestingInjector.resolveConstructorParams (../node_modules/#nestjs/core/injector/injector.js:121:27)
at TestingInjector.loadInstance (../node_modules/#nestjs/core/injector/injector.js:52:9)
at TestingInjector.loadProvider (../node_modules/#nestjs/core/injector/injector.js:74:9)
at async Promise.all (index 3)
● PushService › sendFCM › should return an array of Semesters
Nest can't resolve dependencies of the PushService (FIREBASE_ADMIN_INJECT, ?). Please make sure that the argument HttpService at index [1] is available in the RootTestModule context.
Potential solutions:
- If HttpService is a provider, is it part of the current RootTestModule?
- If HttpService is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing HttpService */ ]
})
at TestingInjector.lookupComponentInParentModules (../node_modules/#nestjs/core/injector/injector.js:231:19)
at TestingInjector.resolveComponentInstance (../node_modules/#nestjs/core/injector/injector.js:184:33)
at TestingInjector.resolveComponentInstance (../node_modules/#nestjs/testing/testing-injector.js:16:45)
at resolveParam (../node_modules/#nestjs/core/injector/injector.js:106:38)
at async Promise.all (index 1)
at TestingInjector.resolveConstructorParams (../node_modules/#nestjs/core/injector/injector.js:121:27)
at TestingInjector.loadInstance (../node_modules/#nestjs/core/injector/injector.js:52:9)
at TestingInjector.loadProvider (../node_modules/#nestjs/core/injector/injector.js:74:9)
at async Promise.all (index 3)
[push.service.spec.ts]
import { Test, TestingModule } from '#nestjs/testing';
import { FirebaseAdminModule } from '#tfarras/nestjs-firebase-admin';
import { SendFcmReqDto } from './dtos/send-fcm.dto';
import { PushService } from './push.service';
import * as admin from 'firebase-admin';
describe('PushService', () => {
let service: PushService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
FirebaseAdminModule.forRootAsync({
useFactory: () => ({
credential: admin.credential.applicationDefault()
})
}),
],
providers: [PushService],
}).compile();
service = module.get<PushService>(PushService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('sendFCM', () => {
const sendFcmReqDto: SendFcmReqDto = {
notification_type: 'type_1',
subject: 'FCM Push Unit Test',
content: 'type1',
small_icon_url: '',
large_icon_url: '',
action1: '',
action2: '',
big_pic_url: '',
is_expand: 'true',
is_sicon: 'false',
is_licon: 'false'
}
it('should return an array of Semesters', async () => {
`const response = service.sendFCM(sendFcmReqDto);`
expect((await response).success).toBe(true);
})
})
});
[push.module.ts]
import { Module } from '#nestjs/common';
import { PushController } from './push.controller';
import { PushService } from './push.service';
import { HttpModule } from '#nestjs/axios';
import { FirebaseAdminModule } from '#tfarras/nestjs-firebase-admin';
import * as admin from 'firebase-admin';
#Module({
imports: [
HttpModule.register({
timeout: 5000,
maxRedirects: 5,
}),
FirebaseAdminModule.forRootAsync({
useFactory: () => ({
credential: admin.credential.applicationDefault()
})
}),
],
controllers: [PushController],
exports: [PushService],
providers: [PushService],
})
export class PushModule {}
import { Module } from '#nestjs/common';
import { PushController } from './push.controller';
import { PushService } from './push.service';
import { HttpModule } from '#nestjs/axios';
import { FirebaseAdminModule } from '#tfarras/nestjs-firebase-admin';
import * as admin from 'firebase-admin';
#Module({
imports: [
HttpModule.register({
timeout: 5000,
maxRedirects: 5,
}),
FirebaseAdminModule.forRootAsync({
useFactory: () => ({
credential: admin.credential.applicationDefault()
})
}),
],
controllers: [PushController],
exports: [PushService],
providers: [PushService],
})
export class PushModule {}
[push.controller.ts]
import { Body, Controller, Post } from '#nestjs/common';
import { PushService } from './push.service';
import { SendFcmReqDto } from './dtos/send-fcm.dto';
#Controller('firebase')
#Controller({ path: 'push', version: '10' })
export class PushController {
constructor(private pushService: PushService) {}
#Post('push')
sendFCM(#Body() fcmReq: SendFcmReqDto) {
return this.pushService.sendFCM(fcmReq);
}
}
[push.service.ts]
import {
BadRequestException,
Injectable,
Inject,
Logger,
NotImplementedException,
} from '#nestjs/common';
import { HttpService } from '#nestjs/axios';
import { resourceLimits } from 'worker_threads';
import { FIREBASE_ADMIN_INJECT, FirebaseAdminSDK } from '#tfarras/nestjs-firebase-admin';
import { SendFcmReqDto } from './dtos/send-fcm.dto';
import { User } from 'src/entities/User';
import { Repository } from 'typeorm';
import { InjectRepository } from '#nestjs/typeorm';
#Injectable()
export class PushService {
[x: string]: any;
private readonly logger = new Logger('PushService');
constructor(
#Inject(FIREBASE_ADMIN_INJECT)
private firebaseAdmin: FirebaseAdminSDK,
private readonly httpService: HttpService,
) {}
async sendFCM({
notification_type,
subject,
content,
small_icon_url,
large_icon_url,
action1,
action2,
big_pic_url,
is_expand,
is_sicon,
is_licon
}: SendFcmReqDto) {
this.logger.log('Start sendFCM()!');
const users = await this.usersRepository.find(
{select: ['fbToken']},
);
const ids: string[] = [];
users.forEach ((user) => {
ids.push(user.fbToken);
});
// const aps
const apsPayload = {
sound:'default',
contentAvailable:1,
alert:'default'
};
const dataPayload = {
noti_type: notification_type,
title: subject,
body: content,
s_icon_url: small_icon_url,
l_icon_url: large_icon_url,
act1: action1,
act2: action2,
b_pic_url: big_pic_url,
is_expand: is_expand,
is_sicon: is_sicon,
is_licon: is_licon
}
const multicast_message = {
tokens: ids,
priority:'high',
aps:apsPayload,
data:dataPayload,
};
await this.firebaseAdmin
.messaging()
.sendMulticast(multicast_message)
.then((response)=> {
this.logger.log('send_response:' + response);
return {
success: true,
result: response,
};
})
.catch((error) => {
this.logger.log('send_response:' + error);
return {
success: false,
result: error,
};
})
}
}
Related
Setup mailhog with docker-compose like:
version: '3'
services:
mailhog:
image: mailhog/mailhog
ports:
- 8025:8025
- 1025:1025
It's possible to access localhost:8025 from browser. Maybe the SMTP server 1025 also works but don't know how to confirm it.
In a NestJS application, for testing the email code as:
#Module({
imports: [NodeMailerModule],
providers: [MailHogEmailRepository],
exports: [MailHogEmailRepository],
})
class MailHogEmailRepositoryModule {}
#Module({
imports: [MailHogEmailRepositoryModule],
providers: [
{
provide: EmailRepository,
useFactory: (
config: ConfigService,
mailHog: MailHogEmailRepository,
) => {
return mailHog;
}
},
inject: [ConfigService, MailHogEmailRepository],
},
],
exports: [EmailRepository],
})
export class EmailRepositoryModule {}
MailHogEmailRepository send with nodemailer:
#Injectable()
export class MailHogEmailRepository implements EmailRepository {
constructor(
#Inject(NodeMailerToken) private readonly nodemailer: Transporter,
) {}
async send(email: Email) {
const options = {
to: email.to,
from: email.from,
subject: email.subject,
};
await this.nodemailer.sendMail(options);
}
}
nodemailer config:
import { Module } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { createTransport } from 'nodemailer';
export const NodeMailerToken = Symbol('nodemailer');
#Module({
providers: [
{
provide: NodeMailerToken,
useFactory: (config: ConfigService) =>
createTransport({
host: 'localhost',
port: 1025,
secure: true,
}),
inject: [ConfigService],
},
],
exports: [NodeMailerToken],
})
export class NodeMailerModule {}
In test source, it always timeout:
import { Test, TestingModule } from '#nestjs/testing';
import request from 'supertest';
import {
FastifyAdapter,
NestFastifyApplication,
} from '#nestjs/platform-fastify';
describe('Test sender', () => {
let app: NestFastifyApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication(new FastifyAdapter());
await app.init();
await app.getHttpAdapter().getInstance().ready();
});
describe('/handler (POST)', () => {
describe('should send data to mail server', () => {
it('success', () => {
const message = ...
return request(app.getHttpServer())
.post('/handler')
.send({ message })
.expect(200);
});
});
});
});
$ npm run test
thrown: "Exceeded timeout of xxx ms for a test.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
It seems the test case couldn't access the mailhog server running in docker container. How to set it correctly?
im trying to build an integration test for a module in NestJS and im having a problem with this package.
I created a redis intances with docker on my local but nothing seems to work.
what am i doing wrong ?
import { config } from '#clients-service/common/config';
import {
DEFAULT_THROTTLE_TTL_SECONDS,
DEFAULT_THROTTLE_LIMIT,
} from '#clients-service/common/constants';
import { RedisCacheModule } from '#clients-service/common/providers/redis-cache';
import { INestApplication } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { ThrottlerModule } from '#nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
const MAX_TIME = 5 * 1000;
describe('[Module] Clients Service', () => {
jest.setTimeout(MAX_TIME);
let app: INestApplication;
beforeAll(async () => {
const test = new ThrottlerStorageRedisService({
host: config.redis.host,
port: config.redis.port,
password: config.redis.password,
});
const module: TestingModule = await Test.createTestingModule({
imports: [
RedisCacheModule,
ThrottlerModule.forRoot({
ttl: DEFAULT_THROTTLE_TTL_SECONDS,
limit: DEFAULT_THROTTLE_LIMIT,
storage: test,
}),
],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should be defined', () => {
expect(app).toBeDefined();
});
});
I am trying to make Jasmine & Karma framework into the current angular application running in ver 8.2. But i am coming across this weird error inside the Karma test running window:
Failed: Unexpected value 'DecoratorFactory' imported by the module 'DynamicTestModule'. Please add a #NgModule annotation.
What is the problem?
My componenent.spec.ts looks like this:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { HomeComponent } from './home.component';
import { NO_ERRORS_SCHEMA} from '#angular/core';
import {RouterTestingModule} from '#angular/router/testing';
import {HttpClientTestingModule} from '#angular/common/http/testing';
import { MsalService } from '#azure/msal-angular';
import { Store } from '#ngrx/store';
import { Pipe } from '#angular/core';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, HttpClientTestingModule, Pipe]
,declarations: [HomeComponent]
,schemas:[NO_ERRORS_SCHEMA]
,providers: [
{provide: MsalService, useFactory: '' },
{provide: Store, useFactory: '' }
]
})
.compileComponents();
}));
it('should have header text', async(() => {
const fixture = TestBed.createComponent(HomeComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
//expect(compiled.querySelector('.header-txt').textContent).toContain('Tax');
}));
});
I found the cause :-
export class MockStore<T> {
private state: BehaviorSubject<T> = new BehaviorSubject(undefined);
setState(data: T) { this.state.next(data); }
select(selector?: any): Observable<T> {
return this.state.asObservable();
}
pipe() {}
dispatch(action: any) { }
}
========================================================================
TestBed.configureTestingModule({
{provide: Store, useFactory: 'MockStore' }
..............
The useFactory property must be some custom class name. Now i mocked the store class.
I am trying to test a ngrx effects in Angular 6 project, I always get error:
Expected $[0].notification.kind = 'C' to equal 'N'.
Expected $[0].notification.hasValue = false to equal true.
I tried this post https://brianflove.com/2018-06-28/ngrx-testing-effects and the one in the ngrx doc. Is there any requirements to make test on effects with ngrx 6 ? The error is not meaningful enough for me. Maybe someone have a complete example about how to do ?
Here's my effect:
initData$: Observable<Action> = this.actions$.pipe(
ofType(INIT_DATA_ACTION),
switchMap((data: any) => {
return this.store.pipe(select(getAppDataResolved)).take(1).switchMap((resolved: any) => {
if (!resolved) {
return this.dataService.getInitData(this.loginService.user.id).switchMap((response: any) => {
return Observable.from([
new ItemsInitDataAction(response.userItems),
new InitDataResolvedAction(),
]);
});
} else {
return Observable.from([
new InitDataResolvedAction(),
]);
}
});
}),
);
and my karma test:
it('should be created', () => {
expect(effects).toBeTruthy(); // got success
});
it('basic test', () => { // got error
const action = new appAction.InitDataAction();
const outcome = new appAction.InitDataResolvedAction();
actions.stream = hot('a', { a: action });
const expected = hot('a', { b: outcome });
expect(effects.initData$).toBeObservable(expected);
});
});
Thanks in advance for helping ;-)
I think you need to insert a mock for the Selector, the next line in the console should be describe something about missing selector data.
let store: Store<any>
class MockStore {
select(){}
}
TestBed.configureTestingModule({
providers: [
{
provide: Store,
useClass: MockStore
}
]
});
store = TestBed.get(Store);
And in test suite you can use Spy to give you any slice of store that you want:
spyOn(store, 'select').and.returnValue(of(initialState));
There is a Typo in expected. It should be 'b' instead of 'a'
const expected = hot('b', { b: outcome });
I havent been able to get a testing working with marbles yet.
Im using Nrwl nx, so my effects test looks like this:
import { TestBed } from '#angular/core/testing';
import { Subject, ReplaySubject } from 'rxjs';
import { EffectsModule } from '#ngrx/effects';
import { StoreModule } from '#ngrx/store';
import { provideMockActions } from '#ngrx/effects/testing';
import { NxModule } from '#nrwl/nx';
import { DataPersistence } from '#nrwl/nx';
import { ChangePasswordEffects } from './change-password.effects';
import { ChangePassword, ChangePasswordSuccessful } from './change-password.actions';
import { HttpClientTestingModule } from '#angular/common/http/testing';
describe('ChangePasswordEffects', () => {
let actions: Subject<any>;
let effects: ChangePasswordEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NxModule.forRoot(), StoreModule.forRoot({}), EffectsModule.forRoot([]), HttpClientTestingModule],
providers: [ChangePasswordEffects, DataPersistence, provideMockActions(() => actions)]
});
effects = TestBed.get(ChangePasswordEffects);
});
describe('loadChangePassword$', () => {
it('should work', () => {
actions = new ReplaySubject(1);
actions.next(ChangePassword);
effects.loadChangePassword$.subscribe(result => {
expect(result).toEqual(ChangePasswordSuccessful);
});
});
});
});
And my code looks like this:
import { PasswordChangeError } from './../../models/password-change-error';
import { Injectable } from '#angular/core';
import { Effect, Actions } from '#ngrx/effects';
import { DataPersistence } from '#nrwl/nx';
import { ChangePasswordPartialState } from './change-password.reducer';
import {
ChangePassword,
ChangePasswordSuccessful,
ChangePasswordError,
ChangePasswordActionTypes
} from './change-password.actions';
import { ChangePasswordService } from '../../services/change-password/change-password.service';
import { map } from 'rxjs/operators';
#Injectable()
export class ChangePasswordEffects {
#Effect() loadChangePassword$ = this.dataPersistence.fetch(ChangePasswordActionTypes.ChangePassword, {
run: (action: ChangePassword, state: ChangePasswordPartialState) => {
return this.passwordService
.changePassword(action.newPassword, action.userId)
.pipe(map(res => new ChangePasswordSuccessful(res)));
},
onError: (action: ChangePassword, error: PasswordChangeError) => {
return new ChangePasswordError(error);
}
});
constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<ChangePasswordPartialState>,
private passwordService: ChangePasswordService
) {}
}
There are plenty of tutorials how to test effects in ngrx 3.
However, I've found only 1 or 2 for ngrx4 (where they removed the classical approach via EffectsTestingModule ), e.g. the official tutorial
However, in my case their approach doesn't work.
effects.spec.ts (under src/modules/list/store/list in the link below)
describe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async() => {
const item = makeItem(Faker.random.word);
actions = hot('--a-', { a: new AddItem({ item })});
const expected = cold('--b', { b: new AddUpdateItemSuccess({ item }) });
// comparing marbles
expect(effects.addItem$).toBeObservable(expected);
});
})
effects.ts (under src/modules/list/store/list in the link below)
...
#Effect() addItem$ = this._actions$
.ofType(ADD_ITEM)
.map<AddItem, {item: Item}>(action => {
return action.payload
})
.mergeMap<{item: Item}, Observable<Item>>(payload => {
return Observable.fromPromise(this._listService.add(payload.item))
})
.map<any, AddUpdateItemSuccess>(item => {
return new AddUpdateItemSuccess({
item,
})
});
...
Error
should return LoadItemsSuccess action for each item
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: AddUpdateItemSuccess({ payload: Object({ item: Object({ title: Function }) }), type: 'ADD_UPDATE_ITEM_SUCCESS' }), error: undefined, hasValue: true }) }).
at compare (webpack:///node_modules/jasmine-marbles/index.js:82:0 <- karma-test-shim.js:159059:33)
at Object.<anonymous> (webpack:///src/modules/list/store/list/effects.spec.ts:58:31 <- karma-test-shim.js:131230:42)
at step (karma-test-shim.js:131170:23)
NOTE: the effects use a service which involves writing to PouchDB. However, the issue doesn't seem related to that
and also the effects work in the running app.
The full code is a Ionic 3 app and be found here (just clone, npm i and npm run test)
UPDATE:
With ReplaySubject it works, but not with hot/cold marbles
const item = makeItem(Faker.random.word);
actions = new ReplaySubject(1) // = Observable + Observer, 1 = buffer size
actions.next(new AddItem({ item }));
effects.addItem$.subscribe(result => {
expect(result).toEqual(new AddUpdateItemSuccess({ item }));
});
My question was answered by #phillipzada at the Github issue I posted.
For anyone checking this out later, I report here the answer:
Looks like this is a RxJS issue when using promises using marbles. https://stackoverflow.com/a/46313743/4148561
I did manage to do a bit of a hack which should work, however, you will need to put a separate test the service is being called unless you can update the service to return an observable instead of a promise.
Essentially what I did was extract the Observable.fromPromise call into its own "internal function" which we can mock to simulate a call to the service, then it looks from there.
This way you can test the internal function _addItem without using marbles.
Effect
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
import { Action } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
export const ADD_ITEM = 'Add Item';
export const ADD_UPDATE_ITEM_SUCCESS = 'Add Item Success';
export class AddItem implements Action {
type: string = ADD_ITEM;
constructor(public payload: { item: any }) { }
}
export class AddUpdateItemSuccess implements Action {
type: string = ADD_UPDATE_ITEM_SUCCESS;
constructor(public payload: { item: any }) { }
}
export class Item {
}
export class ListingService {
add(item: Item) {
return new Promise((resolve, reject) => { resolve(item); });
}
}
#Injectable()
export class SutEffect {
_addItem(payload: { item: Item }) {
return Observable.fromPromise(this._listService.add(payload.item));
}
#Effect() addItem$ = this._actions$
.ofType<AddItem>(ADD_ITEM)
.map(action => action.payload)
.mergeMap<{ item: Item }, Observable<Item>>(payload => {
return this._addItem(payload).map(item => new AddUpdateItemSuccess({
item,
}));
});
constructor(
private _actions$: Actions,
private _listService: ListingService) {
}
}
Spec
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
import { async, TestBed } from '#angular/core/testing';
import { Actions } from '#ngrx/effects';
import { Store, StoreModule } from '#ngrx/store';
import { getTestActions, TestActions } from 'app/tests/sut.helpers';
import { AddItem, AddUpdateItemSuccess, ListingService, SutEffect } from './sut.effect';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
describe('Effect Tests', () => {
let store: Store<any>;
let storeSpy: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({})
],
providers: [
SutEffect,
{
provide: ListingService,
useValue: jasmine.createSpyObj('ListingService', ['add'])
},
{
provide: Actions,
useFactory: getTestActions
}
]
});
store = TestBed.get(Store);
storeSpy = spyOn(store, 'dispatch').and.callThrough();
storeSpy = spyOn(store, 'select').and.callThrough();
}));
function setup() {
return {
effects: TestBed.get(SutEffect) as SutEffect,
listingService: TestBed.get(ListingService) as jasmine.SpyObj<ListingService>,
actions$: TestBed.get(Actions) as TestActions
};
}
fdescribe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async () => {
const { effects, listingService, actions$ } = setup();
const action = new AddItem({ item: 'test' });
const completion = new AddUpdateItemSuccess({ item: 'test' });
// mock this function which we can test later on, due to the promise issue
spyOn(effects, '_addItem').and.returnValue(Observable.of('test'));
actions$.stream = hot('-a|', { a: action });
const expected = cold('-b|', { b: completion });
expect(effects.addItem$).toBeObservable(expected);
expect(effects._addItem).toHaveBeenCalled();
});
})
})
Helpers
import { Actions } from '#ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { empty } from 'rxjs/observable/empty';
export class TestActions extends Actions {
constructor() {
super(empty());
}
set stream(source: Observable<any>) {
this.source = source;
}
}
export function getTestActions() {
return new TestActions();
}