How can I mock nested function with Jasmine& Angular - javascript

constructor(private searchService: SearchService, private storageService: StorageService) {
this.searchService.pagesOutput.subscribe(images => {
this.images = images;
this.pages = this.searchService.numberOfPages;
})
}
I have a service as a dependency injection. I would like to mock it for test.
I know how to mock service and some methods
searchServiceMock = jasmine.createSpyObj('SearchService', ['pagesOutput']);
searchServiceMock.pagesOutput.and.returnValue(someValue)
But how can I reach nested functions?(searchServiceMock.pagesOutput.subscribe?)

Since you're going to be subscribing to the value, if you return an observable, the subscribe will work. To mock an observable, I use of.
import { of } from 'rxjs';
...
searchServiceMock = jasmine.createSpyObj('SearchService', ['pagesOutput']);
searchServiceMock.pagesOutput.and.returnValue(of(/* whatever value you want to be for images in subscribe callback */))

Related

show the page after getting two different service calls

i want to show the page after getting the results of two
different service calls.service1 and service2 are two different
services
don't want to use second service call inside of first service
subscribe.service1 and service2 are two different services.
this.service1.getProfile1(id).subscribe((data1) => {
console.log(data1);
});
this.service2.getProfile2(id).subscribe((data2) => {
console.log(data2);
});
how to i found i got both service calls ?
You can use forkJoin from rxjs https://www.learnrxjs.io/operators/combination/forkjoin.html
import { forkJoin } from 'rxjs';
forkJoin(
this.service1.getProfile1(id),
this.service2.getProfile2(id)
).subscribe(([profile1, profile2]) => {
console.log(profile1, profile2);
});
You can merge the two observables with a fork join in this way:
import { forkJoin } from 'rxjs';
forkJoin([
this.service1.getProfile1(id),
this.service2.getProfile2(id),
]).subscribe(r => {
const data1 = r[0];
const data2 = r[1];
console.log(data1);
console.log(data2);
});
The calls are serialized, and then observable returns with an array of results. The position of the items reflects the order you created the forkJoin.
Actually in this particular use case you are navigating the user and trying to make a service call with two endpoints. Once you receive a response from those end point you are trying to merge it with fork join and send it to component as a observable
But in your question you are looking for a way to make the http calls even before redirecting to the page. There is a good approach for this use case in angular router.
You can specify what is the http calls which you want to perform even before user taken to a page.
Implement resolve in your service level and define that service under route resolve.
Example :
import { Injectable } from '#angular/core';
import { APIService } from './api.service';
import { Resolve } from '#angular/router';
import { ActivatedRouteSnapshot } from '#angular/router';
#Injectable()
export class APIResolver implements Resolve<any> {
constructor(private apiService: APIService) {}
resolve(route: ActivatedRouteSnapshot) {
return this.apiService.getItems(route.params.date);
}
}
Routes :
{
path: 'items/:date',
component: ItemsComponent,
resolve: { items: APIResolver }
}

Testing: spyOn Helper class in angular

Is it possible to spyOn helper class? In the below code, StatefulPatternService.init() is calling a WebSocketHelper.
I would like to spyOn WebSocketHelper and mock the subscribeFn
export class WebSocketHelper{
private url: string;
constructor(url){
this.url = url;
init();
}
init(){
// init websocket & other login
}
}
#Injectable({
providedIn: 'root'
})
export class StatefulPatternService {
constructor(){}
private callback(_data){ }
init(){
let wsHelper = new WebSocketHelper('/subscribe/topic'); // <-- How to spyOn???
wsHelper.subscribeFn = this.callback;
// ...
}
}
If spyOn won't be possible, then how it can be re-written so that this test can be covered?
Your challenge will be getting a hold of 'wsHelper' in order to spy on it. One thought: can you refactor to make wsHelper a class-scope variable instead? Then you could spyOn it when you get the service in the test suite, for example, something like:
service = TestBed.get(StatefulPatternService);
let spy = spyOn(service.wsHelper, 'subscribeFn');
Update
From the comments to my answer it looks like what you are really trying to do is verify that the constructor was called with the proper url. Since you are saving that in a class variable, there should be no need to spy on the constructor, but rather just test the value of the saved variable. As I mentioned in the comments, to do this you will need two things: to make wsHelper a class level variable, and to add a method on the WebSocketHelper class that returns the value of the private variable 'url' so you can test it. I've set up a stackblitz to demonstrate what I'm talking about here: STACKBLITZ Here is a snippet from that stackblitz:
describe('MyService', () => {
let myService: StatefulPatternService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [/* any imports needed */],
providers: [ StatefulPatternService ]
});
myService = TestBed.get(StatefulPatternService);
});
it('should be createable', () => {
expect(myService).toBeTruthy();
});
it('should construct wsHelper properly', () => {
myService.init();
expect(myService.wsHelper.get_url()).toEqual('/subscribe/topic');
})
});

Which RxJS type to use when a method may or may not fetch data asynchronously?

Imagine we have the following factory:
#Injectable()
export class DataClassFactory {
constructor(
private dataService: DataService,
) { }
public createThing(initialData?: InitialData): AsyncSubject<DataClass> {
let dataClass: AsyncSubject<DataClass> = new AsyncSubject<DataClass>();
if (!!initialData) {
dataClass.next(new DataClass(initialData));
dataClass.complete();
} else {
this.dataService.getData().subscribe((dataResponse) => {
dataClass.next(new ReportRequest(dataResponse));
dataClass.complete();
});
}
}
return dataClass;
}
}
We inject this factory, invoke the createThing method, and subscribe to the response in some component. I originally tried to use a plain Subject, but then I realized that in the case where we already have initial data, next() is called before the response is returned, so the subscriber in the component never gets that value.
My question is: is this correct situation in which to use an AsyncSubject, or is there a different/better way to handle this sort of method that has potential synchronous and asynchronous timelines?
I would do something along these lines
public createThing(initialData?: InitialData): Observable<DataClass | ReportRequest> {
if (!!initialData) {
const data = new DataClass(initialData);
return of(data);
} else {
return this.dataService.getData()
.pipe(map(dataResponse => new ReportRequest(dataResponse));
}
}
Whoever calls createThing would get an Observable to which it would have to subscribe.
This Observable would emit an instance of DataClass if initialData is not null, otherwise it would return and instance of ReportRequest as soon as dataService responds.

add data to the end of a behavior object array Angular 5

I have some data that I want to be shared with my entire app so I have created a service like so..
user.service
userDataSource = BehaviorSubject<Array<any>>([]);
userData = this.userDataSource.asObservable();
updateUserData(data) {
this.userDataSource.next(data);
}
then in my component Im getting some data from an api and then sending that data to userDataSource like so..
constructor(
private: userService: UserService,
private: api: Api
){
}
ngOnInit() {
this.api.getData()
.subscribe((data) => {
this.userService.updateUserData(data);
})
}
now that all works but.. I want to be able to add data to the end of the array inside the userDataSource so basically the equivalent of a .push am I able to just call the updateUserData() function and add more data or will doing that overwrite what is currently in there?
Any help would be appreciated
You can add a new method to your service like addData in which you can combine your previous data with new data like.
import {Injectable} from '#angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
#Injectable()
export class UserService {
userDataSource: BehaviorSubject<Array<any>> = new BehaviorSubject([]);
userData = this.userDataSource.asObservable();
updateUserData(data) {
this.userDataSource.next(data);
}
addData(dataObj) {
const currentValue = this.userDataSource.value;
const updatedValue = [...currentValue, dataObj];
this.userDataSource.next(updatedValue);
}
}
For someone that may come accross this issue with a BehaviorSubject<YourObject[]>.
I found in this article a way to properly add the new array of YourObject
import { Observable, BehaviorSubject } from 'rxjs';
import { YourObject} from './location';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class ObjService {
private theObjData: BehaviorSubject<YourObject[]> = new BehaviorSubject<YourObject[]>(null);
constructor() {
}
public SetObjData(newValue: YourObject[]): void {
this.theObjData.next(Object.assign([], newValue));
}
}
How to update data:
// inside some component
this.api.userData().subscribe((results:YourObject) =>
this.objService.SetObjData(results);
)
How to observe changes on other component
// inside another component
ngOnInit() {
this.objService.GetAccountStatements().subscribe((results) =>
...
)
}
Normally Observables and Subjects are meant to be streams of data, not an assignment of data. BehaviorSubjects are different because they hold their last emitted value.
Normally Subjects or BehaviorSubjects inside of a contained class (like a Service) do not want to expose themselves publicly to any other classes, so it's best practice to access their properties with getters or methods. This keeps the data stream cold to all subscribers.
However, since the BehaviorSubject holds the last emitted value, there's a few options here. If all subscribers need a concatenated stream of data from every emission, you could access the last emitted value and append to it:
userDataSource = BehaviorSubject<any[]>([]);
userData = this.userDataSource.asObservable();
updateUserData(data) {
this.userDataSource.next(this.userDataSource.value.push(data));
}
...or, in what might be considered better practice, Subscribers to this Subject could do their own transformation on the stream:
this.api.userData()
.scan((prev, current) => prev.push(current). [])
.subscribe((data) => {
this.concatenatedUserData = data;
});
Use concat to add object
userDataSource = BehaviorSubject<Array<any>>([]);
updateUserData(data) {
this.userDataSource.next(this.userDataSource.value.concat(data));
}
Use filter to remove object
removeUserData(data) {
this.userDataSource.next(this.userDataSource.value.filter(obj => obj !== data));
}

How to use data from an observable in other methods?

I am working on an Angular4 app.
Here is a service I am using to get the data-
export class BookingService {
constructor(private http: HttpClient) {}
getMemberBookings(memberID: number): Observable<any> {
return this.http.get('http://myapi.com/bookings/member/'+memberID).map(response => response['bookings']);
}
}
And then in my component-
export class PrintButtonComponent implements OnInit {
bookings: any;
constructor(private service: BookingService) {}
ngOnInit() {}
downloadPDF() {
this.getBookings(memberID);
//pdf creation logic
}
getBookings(memberID: number) {
this.service.getMemberBookings(memberID).subscribe(data => this.bookings = data);
}
}
The problem is I want to use the data from the service in the downloadPDF method as there is other data in it that will be needed to create the PDF.
But when I return the data from the subscribe or set it to a property, it is giving undefined. I understand that this is due to asynchronous nature, but I dont want to put my pdf creation logic inside the subscribe method.
So how do I solve this problem? I am quite new to Angular and observables.
Thank you.
Since the code above doesn't involve multiple values per observable and doesn't require to stick to them, this can be done with async..await with no extra nesting:
async downloadPDF() {
await this.getBookings(memberID);
//pdf creation logic
}
async getBookings(memberID: number) {
this.bookings = await this.service.getMemberBookings(memberID).toPromise();
}
As any other RxJS operator, toPromise has to be imported.

Categories

Resources