Reputation: 3074
In Angular 9, I have a loader service like below.
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
export interface LoaderState {
show: boolean;
}
@Injectable({
providedIn: 'root'
})
export class LoaderService {
private loaderSubject = new Subject<LoaderState>();
loaderState = this.loaderSubject.asObservable();
constructor() { }
show() {
this.loaderSubject.next({show: true} as LoaderState);
}
hide() {
this.loaderSubject.next({show: false} as LoaderState);
}
}
I want to test the show()
and hide()
methods. For that, I have written a spec like below.
it('should show', (done) => {
spyOn(loaderService, 'show').and.callThrough();
loaderService.show();
expect(loaderService.show).toHaveBeenCalled();
loaderService.loaderState.subscribe((state) => {
expect(state).toBe({show: true});
done();
});
});
But I get below error when I run this
Chrome 86.0.4240 (Windows 10.0.0) LoaderService should show FAILED Error: Timeout - Async callback was not invoked within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
I have searched a lot before asking this question. But can not seem to find the right solution. I am new to Jasmin Unit testing. Any help is appreciated.
Edit: Posting complete spec file for reference.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoaderComponent } from './loader.component';
import { LoaderService } from './loader.service';
describe('LoaderService', () => {
let component: LoaderComponent;
let fixture: ComponentFixture<LoaderComponent>;
let loaderService: LoaderService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoaderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
loaderService = TestBed.inject(LoaderService);
});
it('should be create', () => {
expect(loaderService).toBeTruthy();
});
it('should show', (done) => {
spyOn(loaderService, 'show').and.callThrough();
loaderService.show();
expect(loaderService.show).toHaveBeenCalled();
loaderService.loaderState.subscribe((state) => {
expect(state).toBe({show: true});
done();
});
});
});
Upvotes: 1
Views: 9852
Reputation: 1138
Here's my solution to this pattern:
Service Class
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MyService {
private myServiceEvent = new Subject<any>();
constructor() { }
success(): Observable<any> {
return this.myServiceEvent.asObservable();
}
doSomething(someData: any): void {
// stuff
this.myServiceEvent.next(someData);
}
}
Component that uses the service (the observable would be triggered by some other component or service calling the doSomething
method on the service)
import { Component, OnInit } from '@angular/core';
import { MyService } from ./my-service.ts
@Component({
selector: 'app-mycomponent',
templateUrl: './app-mycomponent.component.html',
styleUrls: ['./app-mycomponent.component.scss']
})
export class AppMyComponent implements OnInit {
private test = '';
constructor(
private myService: MyService,
) { }
ngOnInit() {
this.myService.success().subscribe(data => {
test = data;
});
}
}
Spec that tests the component using the service's Observable response. Note the construction of the service mock and how the Subject variable is used to trigger the Observable.
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Observable, Subject } from 'rxjs';
import { AppMyComponent } from './app-my.component';
import { MyService } from ./my-service.ts
describe('AppMyComponent', () => {
let component: AppMyComponent;
let fixture: ComponentFixture<AppMyComponent>;
const myServiceSubject = new Subject<any>();
const myServiceMock = jasmine.createSpyObj('MyService', [], {
success: () => myServiceSubject.asObservable()
});
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
schemas: [ CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
declarations: [ AppMyComponent ],
providers: [
{ provide: MyService, useValue: myServiceMock }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should execute subscription to MyService success event', fakeAsync(() => {
myServiceSubject.next('somedata');
tick();
expect(componenet.test).toEqual('somedata');
}));
Upvotes: 0
Reputation: 577
The problem is that you're calling subscribe after the actual method call. So when you call show();
there is nothing subscribed to that event, and since the done() callback is inside of it... it's never called.
it('should show', (done) => {
service.loaderState.subscribe((state) => { //subscribe first
expect(state).toEqual({show: true}); // change toEqual instead of toBe since you're comparing objects
done();
});
spyOn(service, 'show').and.callThrough();
service.show(); // invoke after
expect(service.show).toHaveBeenCalled();
});
Upvotes: 5