Manolis Manolis - 14 days ago 10
Javascript Question

Angular 2 Observable and Promise callback unit testing

I have a method in an Angular 2 service, which gets all the details of a specific user account - the one that's logged in - and returns an Observable.

(I am using AngularFire2 for authentication, for what it's worth)

As you can see, the

getData
method is using the
getAuth
method which returns the authentication state (as an observable), which in turn is used by the
getToken
method (which returns a Promise) to get the token which is used to populate the
Authorization
headers and do an http request. (I do understand that my code might require refactoring, and I would appreciate any feedback on that)

getData(): Observable<IData> {
let authHeaders = new Headers();

return Observable.create((o: Observer<IData>) => {
this.getAuth().subscribe((authState: IState) => {
this.getToken(authState).then(token => {
/* ...
* Do things with that token, call an http service etc.
*/ ...
authHeaders.set('Authorization', token);

this.http.get('myendpoint/', {
headers: this.authHeaders
})
.map((response: Response) => response.json())
.map((data: IData) => {
o.next(data);
});
})
.catch((error: Error) => {
Observable.throw(new Error(`Error: ${error}`));
});
});
});
}


I am new to unit testing, and have been trying to test this method, but I still cannot cover the
.catch
of a Promise inside that method.

Here's what my unit test looks like:

describe('Service: UserDataService', () => {
let mockHttp: Http;
let service: UserDataService;
let getAuthSpy: jasmine.Spy;
let getTokenSpy: jasmine.Spy;

beforeEach(() => {
mockHttp = { get: null } as Http;

TestBed.configureTestingModule({
providers: [
{ provide: Http, useValue: mockHttp },
AngularFire,
UserDataService,
]
});

service = new UserDataService(AngularFire, mockHttp);

spyOn(mockHttp, 'get').and.returnValue(Observable.of({
json: () => {
"nickname": "MockNickname"
}
}));

getAuthSpy = spyOn(service, 'getAuth').and.returnValue(Observable.of({
"auth": {
"uid": "12345"
}));

getTokenSpy = spyOn(service, 'getToken').and.returnValue(new Promise((resolve, reject) => {
resolve('test promise response');
}));
});

describe('getLead', () => {
beforeEach(() => {
spyOn(service, 'getLead').and.callThrough();
});

it('should return an object of user data and set the dataStore.userEmail if state exists', () => {
service.getLead().subscribe(res => {
expect(res).toEqual(jasmine.objectContaining({
nickname: 'MockNickname'
}));
});

expect(service.getLead).toHaveBeenCalled();
});

it('should throw, if no authState is provided', () => {
getAuthSpy.and.returnValue(Observable.of(false));

service.getLead();

expect(service.getLead).toThrow();
});

it('should throw, if getToken() fails to return a token', () => {
getTokenSpy.and.returnValue(new Promise((resolve, reject) => {
reject('test error response');
}));

/*
* This is where I am getting lost
*/

service.getLead();

expect(service.getLead).toThrow();
});
});
});


From what I understand I have to somehow make that Promise from
getToken
reject, and test to see if my observable will throw.

Any help or feedback greatly appreciated.

Answer

I don't use either jasmine nor AngularFire2 so I can't tell what exactly you have to do but from you code it's obvious that this.getToken(authState) returns a Promise.

Question is from what library this Promise class comes from (aka what polyfill you're using).

As far as I know implementations of Promises/A should wrap callback calls with try - catch, so if your callback in then(...) throws an exception that the Promise will propagate to catch(). So I guess you could fake response from this.http.get('myendpoint/') with some malformed JSON that would cause an error at response.json().

Second thing is the inner callback for catch():

.catch((error: Error) => {
    Observable.throw(new Error(`Error: ${error}`));
});

This does literally nothing. There's no return statement and Observable.throw() needs to appear inside Observable chain to do something. Note that this .catch() method comes from your Promise class and not from Observable.

If you want it to actually propagate the error to Observers then you have to explicitly call:

.catch((error: Error) => {
    o.error(error);
});

Maybe if the error bubbled up to the Observable.create() it would be passed to the Observer as well, I'm not sure about this.