Unit test Angular service - javascript

I'm struggling with testing a service in an Angular project.
The service is pretty small but i can't figure out how to test the observer in the class.
I am trying to test a method of this class. The public methods must do what it promises to do. When I call the 'pop' method, the test fails.
Since the pop method is public and has a Message as return values, this method should return a message after invoke it. The underlying code is less relegant in the case of testing.
The reason the test fails is because the observer is still undefined at that moment. I suggest that the reason is because the callback method is not executed at the moment when i call the pop method in the test, thus the addMessage observer is not intialized yet.
Since I have just started on this project, I am cautious about assumptions about the code.
Does anyone have a suggestion on how I could test this code?
Is it right that the pop method is public or should it be private?
Edit:
The pop method is used by a few other classes an can't be private.
My question is actualy: Is this implementation of this service right?
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { share} from 'rxjs/operators';
import { Observer } from 'rxjs';
import { Message } from 'primeng/components/common/api';
#Injectable()
export class FeedBackService {
obsAddMessage: Observable;
obsClearMessages: Observable;
/** #internal */
private clearMessages: Observer;
private addMessage: Observer;
/**
* Creates an instance of FeedBackService.
*/
constructor() {
this.obsAddMessage = new Observable(observer => this.addMessage = observer).pipe(share());
this.obsClearMessages = new Observable(observer => this.clearMessages = observer).pipe(share());
}
/**
* Synchronously create and show a new message instance.
*
* #param {(string | Message)} type The type of the message, or a Message object.
* #param {string=} title The message title.
* #param {string=} body The message body.
* #returns {Message}
* The newly created Message instance.
*/
pop( type: string | Message, title?: string, body?: string ): Message {
const message: any = typeof type === 'string' ? {severity: type, summary: title, detail: body} : type;
if (!this.addMessage) {
throw new Error('No Containers have been initialized to receive messages.');
} else {
this.addMessage.next(message);
}
return message;
}
}
The Test:
import {Message} from 'primeng/components/common/api';
import {FeedBackService} from './feedback.service';
fdescribe('Service: Feedback', () => {
let feedbackService: FeedBackService;
const MESSAGE: Message = {severity: 'This is a message', summary: 'Title', detail: 'Body'};
const SEVERITY = 'severity';
const SUMMARY = 'summary';
const DETAIL = 'detail';
beforeEach(() => {
feedbackService = new FeedBackService();
});
it('#pop should return the message when passing in a message', () => {
let returnMessage = feedbackService.pop(MESSAGE);
expect(returnMessage).toEqual(MESSAGE);
});
});
The Error:

Like any Observable, obsAddMessage isn't executed until subscribed to, and therefore as you demonstrated the observer is still undefined if you attempt to push a new value in before subscribing. The solution is simply to set up a subscribe before calling feedbackService.pop().
I set up a simple Stackblitz to show what I mean. The spec from that Stackblitz is:
it('#pop should return the message when passing in a message', () => {
feedbackService.obsAddMessage.subscribe(message => console.log(message));
let returnMessage = feedbackService.pop(MESSAGE);
expect(returnMessage).toEqual(MESSAGE);
});
I hope this helps.

Related

Use store values in ngrx/data

I am using ngrx/data and what I need to do is set a value in the store, let's call this ID. And then when I make any request to an entity to pull that ID from the store. I will use update as an example.
Here is an example of a Client Entity Service. I can easily map the returned data as super.update returns an observable.
import { Injectable } from '#angular/core';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '#ngrx/data';
import { Client } from '../../store/client/client.model';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class ClientEntityService extends EntityCollectionServiceBase<Client> {
constructor(
serviceElementsFactory: EntityCollectionServiceElementsFactory,
) {
super('Client', serviceElementsFactory);
}
public update(entity: Partial<Client>): Observable<Client> {
return super.update(entity);
}
}
However I want to use a store value to make the update. So focusing on the update I can do this:
public update(entity: Partial<Client>): Observable<Client> {
this.store.pipe(
tap((store) => {
console.log(store);
})
).subscribe();
return super.update(entity);
}
Which prints out the store and I can see the value I need, so I could do this
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1)
).subscribe((id) => {
return super.update({
id,
...update
});
});
}
However it is requiring me to subscribe to the observable to be able to trigger it. That would mean the caller could not pipe the results and is generally not ideal.
I am wondering if anyone knows of a good solution to be able to get the data from the store but not have to subscribe like I am doing above to get the data, ideally I would want to use a switchMap like this:
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
switchMap((id) => {
return super.update({
id,
...update
});
}),
take(1)
)
Thanks
You wrote it correctly in your ideal solution. The difference is that you just need to move take(1) to be right after the select.
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1),
switchMap((id) => {
return super.update({
id,
...update
});
}),
)
so store won't cause update requests on every change.
After stepping into the super.update call I could see that the dispatcher was calling:
update(entity, options) {
// update entity might be a partial of T but must at least have its key.
// pass the Update<T> structure as the payload
/** #type {?} */
const update = this.toUpdate(entity);
options = this.setSaveEntityActionOptions(options, this.defaultDispatcherOptions.optimisticUpdate);
/** #type {?} */
const action = this.createEntityAction(EntityOp.SAVE_UPDATE_ONE, update, options);
if (options.isOptimistic) {
this.guard.mustBeUpdate(action);
}
this.dispatch(action);
return this.getResponseData$(options.correlationId).pipe(
// Use the update entity data id to get the entity from the collection
// as might be different from the entity returned from the server
// because the id changed or there are unsaved changes.
map((/**
* #param {?} updateData
* #return {?}
*/
updateData => updateData.changes)), withLatestFrom(this.entityCollection$), map((/**
* #param {?} __0
* #return {?}
*/
([e, collection]) => (/** #type {?} */ (collection.entities[this.selectId((/** #type {?} */ (e)))])))), shareReplay(1));
}
Which effectively just dispatches some actions and then creates a selected observable from this.getResponseData$ using the correlationid etc.
In my use case because I am using the store to get the Id of the current client I don't need the updated client returned as I already have an observable.
On top of the ClientEntityService I have another facade which I am calling ClientService.
Which looks like this:
#Injectable({
providedIn: 'root'
})
export class ClientService {
constructor(
private clientEntityService: ClientEntityService,
private store: Store<AppState>
) {}
public getCurrentClient(): Observable<Client> {
return this.clientEntityService.entityMap$.pipe(
withLatestFrom(this.store.pipe(select(getCurrentId))),
map(([clients, currentId]) => clients[currentId])
);
}
public updateCurrentClient(update: Partial<Client>): Subscription {
return this.getCurrentClient().pipe(
take(1),
switchMap((client) => {
return this.clientEntityService.update({
id: client.id,
...update
});
})
).subscribe();
}
}
So now from within my component I have the constructor
constructor(
private clientService: ClientService,
) {
this.client$ = this.clientService.getCurrentClient();
}
And then on update I call:
this.clientService.updateCurrentClient(theUpdate);
And because I already have this.client$ as an observable of that client being updated I don't need updateCurrentClient to return Observable<Client>. So as per above I am just returning Subscription
I could modify updateCurrentClient to implement something similar to what the DefaultDataService returns, but I'd expect that could be subject to change in future versions. So for now. I am happy with this solution.

How can I access the auto-created document ID when creating a new Firestore document in Angular?

I am trying to return the document id when I create it. Since Firebase functions are async the return value is not completed until the query is done. How can I prevent this so I can wait to get the value when the query is done?
This function create the document is located in a service:
public makeDoc(title: string, score: number): any{
const fields = {
title: title,
score: number
}
this.db.collection('saved').add(fields)
.then(function(ref) {
console.log(ref.id);
return ref.id;
})
}
I call this from a function which is located in a component:
onCreate() {
const str = this.createService.makeDoc(this.title, this.score);
console.log(str);
}
Try following:
const fields = {
title: title,
score: number
}
var newFieldsRef = this.db.collection('saved').push();
this.db.collection('saved').child(newFieldsRef).set(fields);
var id = newFieldsRef.toString();
You don't want to prevent waiting until the query is done, you should embrace the use of promises here.
First, if you haven't, make sure you import the firestore namespace in the service:
import { firestore } from 'firebase';
Now, for your service:
I had to slightly change your makeDoc method as the fields object wasn't being created in a valid way (e.g. reusing the number type):
public makeDoc(titleParam: string, scoreParam: number): Promise<firestore.DocumentReference> {
const fields = {
title: titleParam,
score: scoreParam
};
return this.db.collection('saved').add(fields);
}
This now returns a Promise<DocumentReference> which, when resolved, the reference will point to the created document.
Now, the call to it in onCreate looks like:
onCreate() {
this.createService.makeDoc('myTitle', 123)
.then((ref) => { console.log(ref.id); })
.catch((err) => { console.log(err); });
}
And this will log the id as soon as it is available.

Angular2 unable to map a single JSON object?

How do you map and use a JSON reponse that is a single object, rather than an array?
Recently, I started adding a new feature to a project I'm working on that should be taking a JSON response from an API and filling out a simple template with data from it. Shouldn't be difficult, right? Well, no... and yet, yes...
Mock version of the JSON response:
{
"id": 1,
"name": "Acaeris",
}
profile.service.ts
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Profile } from './profile';
/**
* This class provides the Profile service with methods to read profile data
*/
#Injectable()
export class ProfileService {
/**
* Creates a new ProfileService with the injected Http.
* #param {Http} http - The injected Http.
* #constructor
*/
constructor(private http: Http) {}
/**
* Returns an Observable for the HTTP GET request for the JSON resource.
* #return {Profile} The Observable for the HTTP request.
*/
get(): Observable<Profile> {
return this.http.get('assets/profile.json')
.map(res => <Profile>res.json())
.catch(this.handleError);
}
/**
* Handle HTTP error
*/
private handleError (error: any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg);
return Observable.throw(errMsg);
}
}
profile.component.ts
import { Component, OnInit } from '#angular/core';
import { ProfileService } from '../services/profile/profile.service';
import { Profile } from '../services/profile/profile';
/**
* This class represents the lazy loaded ProfileComponent
*/
#Component({
moduleId: module.id,
selector: 'sd-profile',
templateUrl: 'profile.component.html',
styleUrls: ['profile.component.css'],
})
export class ProfileComponent implements OnInit {
errorMessage: string;
profile: Profile;
/**
* Creates an instance of the ProfileComponent with the injected
* ProfileService
*
* #param {ProfileService} profileService - The injected ProfileService
*/
constructor(public profileService: ProfileService) {}
/**
* Get the profile data
*/
ngOnInit() {
this.getProfile();
}
/**
* Handles the profileService observable
*/
getProfile() {
this.profileService.get()
.subscribe(
data => this.profile = data,
error => this.errorMessage = <any>error
);
}
}
profile.ts
export interface Profile {
id: number;
name: string;
}
And I'm just trying to output it using {{profile.name}} but this ends up with the console showing a whole load of error messages and no output. If I try to check the contents of profile after it has loaded, it tells me it is undefined.
However, here's the confusing part. If I replace all the Profile references to Profile[], wrap the JSON in an array, add *ngFor="let p of profile" abd use {{p.name}} everything works fine. Unfortunately, in the actual finished application I would not have control of the JSON format. So what am I doing wrong when trying to handle it as a single object in comparison to handling as an array of objects?
Looks like at expression {{profile.name}} profile variable is undefined at page rendering moment. You can try either add some getter like this:
get profileName(): string { return this.profile ? this.profile.name ? ''; }
and use at template {{profileName}} or you can use ngIf at template like this:
<div *ngIf="profile">{{profile.name}}</div>
or shorter (as drewmoore suggested at comment below):
<div>{{profile?.name}}</div>
When you are working with array it is the same situation - at first rendering time array is undefined. ngFor handles this for you and renders nothing. When async operation of getting 'profile items' is complete - UI is rerendered again with correct values.
The mapfunction returns Observables which are a collection of elements. It basically work the same way as the map function for arrays.
Now to solve you can replace the Profile references by Profile[] and use {{profile[0].name}}.

Why am I receiving a typescript error when migrating a Paho MQTT function from Angular 1 to Angular 2?

I am attempting to do several things on connecting to my MQTT broker, I have created an mqtt provider in my ionic 2, angular 2 application, the provider is given below:
import { Component } from '#angular/core';
import { NavController, ViewController } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';
import { Paho } from 'ng2-mqtt/mqttws31';
#Component({
selector: 'page-greywater',
templateUrl: 'greywater.html'
})
export class MQTT_Greenchain {
private _client: Paho.MQTT.Client;
private options = {
userName: 'rdjghvoh',
password: 'w7Ex0VTqZViw',
timeout: 30,
useSSL:true,
onSuccess:this.onConnected,
};
private topic: string;
public displayedMessage: string;
public mes: Paho.MQTT.Message;
public constructor() {
this._client = new Paho.MQTT.Client(
"m20.cloudmqtt.com",
Number(30775),
"",
"peter"
);
this._client.onConnectionLost = (responseObject: {errorCode: Number, errorMessage: string}) => {
console.log('poes');
console.log(responseObject.errorMessage);
};
this._client.onMessageArrived = (message: Paho.MQTT.Message) => {
this.onMessageArr(message);
console.log('Message arrived.');
};
this.topic = "haha";
this.displayedMessage = "what I was";
}
connectMe() {
console.log("MQTT OPTIONS: " + this.options);
this._client.connect(this.options);
}
private onConnected(): void {
console.log('Connected to broker.');
this._client.subscribe(this.topic);
this.mes = new Paho.MQTT.Message("-1"); // -1 => Notify
this.mes.destinationName = this.topic;
this._client.send(this.mes);
}
private onMessageArr(message: Paho.MQTT.Message){
this.displayedMessage = message.payloadString;
}
}
I have been able to call the following in angular 1 without trouble, and I was able to get everything MQTT-related, working. The function in angular 1 is as follows:
function onConnect() {
sharedUtils.hideLoading();
console.log("onConnect, CURRENT TOPIC: " + mqttData);
client.subscribe(mqttData.currentTopic);
}
In the above, mqttData.currentTopic is merely a string.
The function accepts 1 argument, even though it can accept 2 (an options object).
In angular 2, typescript gives me an error:
Supplied parameters do not match any signature of call target
Why is it not allowing me to call the function with one argument as in angular 1? If I give it {} as a second argument:
this._client.subscribe(this.topic, {});
I am given the error that:
AMQJS0005E Internal error. Error Message: Cannot read property 'subscribe' of undefined, Stack trace: TypeError: Cannot read property 'subscribe' of undefined
This is the error received in the response object parameter, passed to the onConnectionLost callback function.
I am quite certain that my 'this._client' is not undefined since the message 'Connected to broker.' appears in the console, indicating that onConnected, the onSuccess property callback of the connect method was obviously called?
What am I not getting here?
Try this and reply if it is working fine
this.client.subscribe(this.topic, '');

How to stub a method of jasmine mock object?

According to the Jasmine documentation, a mock can be created like this:
jasmine.createSpyObj(someObject, ['method1', 'method2', ... ]);
How do you stub one of these methods? For example, if you want to test what happens when a method throws an exception, how would you do that?
You have to chain method1, method2 as EricG commented, but not with andCallThrough() (or and.callThrough() in version 2.0). It will delegate to real implementation.
In this case you need to chain with and.callFake() and pass the function you want to be called (can throw exception or whatever you want):
var someObject = jasmine.createSpyObj('someObject', [ 'method1', 'method2' ]);
someObject.method1.and.callFake(function() {
throw 'an-exception';
});
And then you can verify:
expect(yourFncCallingMethod1).toThrow('an-exception');
If you are using Typescript, it's helpful to cast the method as Jasmine.Spy. In the above Answer (oddly I don't have rep for comment):
(someObject.method1 as Jasmine.Spy).and.callFake(function() {
throw 'an-exception';
});
I don't know if I'm over-engineering, because I lack the knowledge...
For Typescript, I want:
Intellisense from the underlying type
The ability to mock just the methods used in a function
I've found this useful:
namespace Services {
class LogService {
info(message: string, ...optionalParams: any[]) {
if (optionalParams && optionalParams.length > 0) {
console.log(message, optionalParams);
return;
}
console.log(message);
}
}
}
class ExampleSystemUnderTest {
constructor(private log: Services.LogService) {
}
doIt() {
this.log.info('done');
}
}
// I export this in a common test file
// with other utils that all tests import
const asSpy = f => <jasmine.Spy>f;
describe('SomeTest', () => {
let log: Services.LogService;
let sut: ExampleSystemUnderTest;
// ARRANGE
beforeEach(() => {
log = jasmine.createSpyObj('log', ['info', 'error']);
sut = new ExampleSystemUnderTest(log);
});
it('should do', () => {
// ACT
sut.doIt();
// ASSERT
expect(asSpy(log.error)).not.toHaveBeenCalled();
expect(asSpy(log.info)).toHaveBeenCalledTimes(1);
expect(asSpy(log.info).calls.allArgs()).toEqual([
['done']
]);
});
});
Angular 9
Using jasmine.createSpyObj is ideal when testing a component where a simple service is injected. For example: let's say, in my HomeComponent I have a HomeService (injected). The only method in the HomeService is getAddress().
When creating the HomeComponent test suite, I can initialize the component and service as:
describe('Home Component', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let element: DebugElement;
let homeServiceSpy: any;
let homeService: any;
beforeEach(async(() => {
homeServiceSpy = jasmine.createSpyObj('HomeService', ['getAddress']);
TestBed.configureTestingModule({
declarations: [HomeComponent],
providers: [{ provide: HomeService, useValue: homeServiceSpy }]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
homeService = TestBed.get(HomeService);
fixture.detectChanges();
});
}));
it('should be created', () => {
expect(component).toBeTruthy();
});
it("should display home address", () => {
homeService.getAddress.and.returnValue(of('1221 Hub Street'));
fixture.detectChanges();
const address = element.queryAll(By.css(".address"));
expect(address[0].nativeNode.innerText).toEqual('1221 Hub Street');
});
});
This is a simple way to test your component using jasmine.createSpyObj. However, if your service has more methods more complex logic, I would recommend creating a mockService instead of createSpyObj. For example:
providers: [{ provide: HomeService, useValue: MockHomeService }]
Hope this helps!
Building on #Eric Swanson's answer, I've created a better readable and documented function for using in my tests. I also added some type safety by typing the parameter as a function.
I would recommend to place this code somewhere in a common test class, so that you can import it in every test file that needs it.
/**
* Transforms the given method into a jasmine spy so that jasmine functions
* can be called on this method without Typescript throwing an error
*
* #example
* `asSpy(translator.getDefaultLang).and.returnValue(null);`
* is equal to
* `(translator.getDefaultLang as jasmine.Spy).and.returnValue(null);`
*
* This function will be mostly used in combination with `jasmine.createSpyObj`, when you want
* to add custom behavior to a by jasmine created method
* #example
* `const translator: TranslateService = jasmine.createSpyObj('TranslateService', ['getDefaultLang'])
* asSpy(translator.getDefaultLang).and.returnValue(null);`
*
* #param {() => any} method - The method that should be types as a jasmine Spy
* #returns {jasmine.Spy} - The newly typed method
*/
export function asSpy(method: () => any): jasmine.Spy {
return method as jasmine.Spy;
}
Usage would be as follows:
import {asSpy} from "location/to/the/method";
const translator: TranslateService = jasmine.createSpyObj('TranslateService', ['getDefaultLang']);
asSpy(translator.getDefaultLang).and.returnValue(null);

Categories