Angular 8 testing error Unexpected value 'DecoratorFactory' imported by the module 'DynamicTestModule' - karma-jasmine

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.

Related

How can I get the tests to run normally (nest, unittestm fcm)?

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,
};
})
}
}

TypeError: Cannot read property 'length' of null on typescript spec file

I am getting TypeError: Cannot read property 'length' of null on testing my component. how to fix this?
here is html:
<header-nav
[eventList]="eventList$ | async"
[eventListSize]="(eventList$ | async).length"
(selectedEvent)="eventSelected($event)"
(setLang)="langChanged($event)"
[currentLang]="currentLang$ | async"
[langList] = "langList$ | async"
(leftNavi) = "leftNaviHandler($event)"
>
here is the ts file:
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { Store, select } from "#ngrx/store";
import { Observable } from "rxjs";
import { ModelEvent, EventState } from "./../../../calendar/models/model.event";
import { ModelLang, ModelLonguage } from "./../../../shared-components/models/models";
import { CalendarActions, Load, EventSelected } from "./../../../calendar/state/calendar.actions";
import * as fromRoot from "./../../../calendar/state";
import * as fromObservables from "./../../state";
import { Lang, LoadLang } from "./../../state/actions/shared.actions";
import { ShowNavi } from "./../../../shared-components/state/actions/shared.actions";
#Component({
selector: 'header-nav-shell',
templateUrl: './header-nav-shell.component.html',
styleUrls: ['./header-nav-shell.component.scss']
})
export class HeaderNavShellComponent implements OnInit {
/**
* declartion of observable events
*/
eventList$:Observable<ModelEvent[]>;
eventListSize$:number;
currentLang$:Observable<string>;
langList$:Observable<ModelLonguage[]>;
constructor(private store:Store<fromRoot.NewState>, private router:Router) { }
ngOnInit() {
this.store.dispatch(new Load());
this.store.dispatch( new LoadLang());
this.eventList$ = this.store.pipe(select(fromRoot.getEvents));
this.currentLang$ = this.store.pipe(select(fromObservables.getCurrentLang));
this.langList$ = this.store.pipe(select(fromObservables.getLangList));
}
eventSelected(event) {
this.store.dispatch(new EventSelected(event));
this.router.navigateByUrl("iboCalendar");
}
langChanged(event) {
this.store.dispatch( new Lang(event.Name));
}
leftNaviHandler(event):void {
this.store.dispatch(new ShowNavi(event));
}
}
here is the spec file:
import { async, fakeAsync, tick, ComponentFixture, TestBed } from '#angular/core/testing';
import { HeaderNavShellComponent } from './header-nav-shell.component';
import { HeaderComponent } from './../../header/header.component';
import { HeaderNavComponent } from './../../components/header-nav/header-nav.component';
import { StoreModule, Store } from '#ngrx/store';
import { TranslateFakeLoader,TranslateLoader,TranslateModule,TranslateService, TranslateStore } from '#ngx-translate/core';
import { RouterTestingModule } from '#angular/router/testing';
import { reducerShared } from "./../../state/reducers/shared.reducer";
import { HttpClientModule, HttpClient } from '#angular/common/http';
describe('HeaderNavShellComponent', () => {
let component: HeaderNavShellComponent;
let fixture: ComponentFixture<HeaderNavShellComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeaderNavShellComponent, HeaderComponent, HeaderNavComponent ],
imports:[
HttpClientModule,
StoreModule.forRoot({}),
RouterTestingModule,
TranslateModule.forChild({
          loader: {
            provide: TranslateLoader,
            useClass: TranslateFakeLoader
          }
         })
],
providers:[TranslateService, TranslateStore, Store, HttpClient ]
})
.compileComponents();
}));
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(HeaderNavShellComponent);
component = fixture.componentInstance;
tick(1000);
fixture.detectChanges();
}));
it('should create', fakeAsync(() => {
expect(component).toBeTruthy();
}));
});
Error:
TypeError: Cannot read property 'length' of null
at Object.eval [as updateDirectives] (ng:///DynamicTestModule/HeaderNavShellComponent.ngfactory.js:34:98)
at Object.debugUpdateDirectives [as updateDirectives] (node_modules/#angular/core/fesm5/core.js:23813:1)
at checkAndUpdateView (node_modules/#angular/core/fesm5/core.js:23209:1)
at callViewAction (node_modules/#angular/core/fesm5/core.js:23450:1)
at execComponentViewsAction (node_modules/#angular/core/fesm5/core.js:23392:1)
at checkAndUpdateView (node_modules/#angular/core/fesm5/core.js:23215:1)
at callWithDebugContext (node_modules/#angular/core/fesm5/core.js:24079:1)
at Object.debugCheckAndUpdateView [as checkAndUpdateView] (node_modules/#angular/core/fesm5/core.js:23781:1)
at ViewRef_.push.../node_modules/#angular/core/fesm5/core.js.ViewRef_.detectChanges (node_modules/#angular/core/fesm5/core.js:21590:1)
at ComponentFixture.push.../node_modules/#angular/core/fesm5/testing.js.ComponentFixture._tick (node_modules/#angular/core/fesm5/testing.js:227:1)
what is wrong i do here? any one figure out me please?
I am getting the length from async value.
Thanks in advance
My case the problem was very similar to the above one, the only difference was not using the async, but the same error
TypeError: Cannot read property 'length' of null
at Object.eval [as updateDirectives] (ng:///DynamicTestModule/HeaderNavShellComponent.ngfactory.js:34:98)
at Object.debugUpdateDirectives [as updateDirectives] (node_modules/#angular/core/fesm5/core.js:23813:1)
You will notice the stack trace is nowhere showing where the actual problem is.
The actual problem was the sanity required for checking the length
[someData]="someObj.list.length"
So changing the above line with this
[someData]="someObj?.list?.length"
That's all, things started working.
Posting this answer for other people stuck with similar problems.
Try creating a stub as below for handling store service:
class StoreStub(val){
if(val === fromRoot.getEvents){
//event action will have values as you are passing "fromRoot.getEvents" in component
return of({
//expected object of events
})
}else if(val === fromObservables.getCurrentLang){
//event action will have values as you are passing "fromObservables.getCurrentLang" in component
return of({
//expected object of events
})
}
// and so on.....
}
and then use useClass:
providers:[TranslateService, TranslateStore, {provide: Store, useClass: StoreStub}, HttpClient ]

Testing NgRx 6 Effects

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
) {}
}

Using AlarmService Throws different Errors in angular 6 TypeScript

I'm having some problem in resolving this error. When i add the Alarmservice i always get this error.
When i checked this error in google, every one say, i have to add this in provider, If i add in provider also it is not working and i get a different error
import { Component, Input } from '#angular/core';
import { Client, BasicAuth, AlarmService } from '#c8y/client';
import { CumulocityService } from './c8y.service';
import {
Router
} from '#angular/router';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
#Input() name: string;
model = {
user: '',
password: '',
tenant: ''
};
error = {
shown: false,
msg: ''
};
disabled = false;
basicAuth: string = 'https://tempar.adamos.com'
currentUser: object = {};
constructor(
private cumulocity: CumulocityService,
private alarmService: AlarmService,
private route: Router
) { }
async login() {
this.disabled = true;
const client = new Client(new BasicAuth(
{
user: this.model.user,
password: this.model.password,
tenant: this.model.tenant
}),
this.basicAuth
);
sessionStorage.setItem("client", JSON.stringify(client));
client.inventory.list$().subscribe((data) => {
// request all inventory data via fetch and adds realtime if data changes
console.log(data);
});
try {
let user = await client.user.current();
debugger;
this.cumulocity.client = client;
console.log(user.data);
const alarmId: number = 63704;
const { data, res } = await this.alarmService.detail(alarmId);
console.log(data, res);
// this.route.navigate(['/dashboard']);
} catch (ex) {
this.cumulocity.client = null;
this.error.shown = true;
this.error.msg = ex.message;
} finally {
this.disabled = false;
}
}
}
core.js:1673 ERROR Error: Uncaught (in promise): Error: StaticInjectorError(AppModule)[LoginComponent -> AlarmService]:
StaticInjectorError(Platform: core)[LoginComponent -> AlarmService]:
NullInjectorError: No provider for AlarmService!
Error: StaticInjectorError(AppModule)[LoginComponent -> AlarmService]:
StaticInjectorError(Platform: core)[LoginComponent -> AlarmService]:
NullInjectorError: No provider for AlarmService!
at NullInjector.push../node_modules/#angular/core/fesm5/core.js.NullInjector.get (core.js:1062)
at resolveToken (core.js:1300)
at tryResolveToken (core.js:1244)
at StaticInjector.push../node_modules/#angular/core/fesm5/core.js.StaticInjector.get (core.js:1141)
at resolveToken (core.js:1300)
at tryResolveToken (core.js:1244)
at StaticInjector.push../node_modules/#angular/core/fesm5/core.js.StaticInjector.get (core.js:1141)
at resolveNgModuleDep (core.js:8376)
at NgModuleRef_.push../node_modules/#angular/core/fesm5/core.js.NgModuleRef_.get (core.js:9064)
at resolveDep (core.js:9429)
at NullInjector.push../node_modules/#angular/core/fesm5/core.js.NullInjector.get (core.js:1062)
at resolveToken (core.js:1300)
at tryResolveToken (core.js:1244)
at StaticInjector.push../node_modules/#angular/core/fesm5/core.js.StaticInjector.get (core.js:1141)
at resolveToken (core.js:1300)
at tryResolveToken (core.js:1244)
at StaticInjector.push../node_modules/#angular/core/fesm5/core.js.StaticInjector.get (core.js:1141)
at resolveNgModuleDep (core.js:8376)
at NgModuleRef_.push../node_modules/#angular/core/fesm5/core.js.NgModuleRef_.get (core.js:9064)
at resolveDep (core.js:9429)
at resolvePromise (zone.js:814)
at resolvePromise (zone.js:771)
at zone.js:873
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
at Object.onInvokeTask (core.js:3815)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:420)
at Zone.push../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188)
at drainMicroTaskQueue (zone.js:595)
If i add Alarmservice at the app.module.ts I get a different error, as said above
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { FormsModule } from '#angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { CumulocityService } from './login/c8y.service';
import { AlarmService } from '#c8y/client';
#NgModule({
declarations: [
AppComponent,
LoginComponent,
DashboardComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule
],
providers: [ AlarmService, CumulocityService],
bootstrap: [AppComponent]
})
export class AppModule { }
ERROR
Uncaught Error: Can't resolve all parameters for AlarmService: (?, ?).
at syntaxError (compiler.js:1016)
at CompileMetadataResolver.push../node_modules/#angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getDependenciesMetadata (compiler.js:10917)
at CompileMetadataResolver.push../node_modules/#angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getTypeMetadata (compiler.js:10810)
at CompileMetadataResolver.push../node_modules/#angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getInjectableTypeMetadata (compiler.js:11032)
at CompileMetadataResolver.push../node_modules/#angular/compiler/fesm5/compiler.js.CompileMetadataResolver.getProviderMetadata (compiler.js:11041)
at compiler.js:10979
at Array.forEach (<anonymous>)
at CompileMetadataResolver.push../node_modules/#angular/compiler/fesm5/compiler.js.CompileMetadataResolver._getProvidersMetadata (compiler.js:10939)
at CompileMetadataResolver.push../node_modules/#angular/compiler/fesm5/compiler.js.CompileMetadataResolver.getNgModuleMetadata (compiler.js:10658)
at JitCompiler.push../node_modules/#angular/compiler/fesm5/compiler.js.JitCompiler._loadModules (compiler.js:23858)
Have you seen the stackblitz example? I guess you have because it looks like the code from there.
At the moment you cannot inject the services into angular directly. You can just write a wrapper around the client (like done in the example)
So to make your code working:
remove the AlarmService import
Request a alarm via this.client.alarm.detail(alarmId); instead of the AlarmService.
The this.client.alarm is of type AlarmService and uses the login you set via new Client(new BasicAuth([...]));.
In future, you can use #c8y/ngx-data to do proper dependency injection, but it is not released as stable and therefore I would not suggest to use it.

How to correctly test effects in ngrx 4?

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();
}