How to test code block inside observable subscription for Angular service - javascript

I have an Angular service method that uses another service for making api call. After the call success and inside of the subscription, I call some methods e.g. notifySucess, emit new value...
My question is how can I test the code block inside the subscription e.g. expect notifySucess to have been called. I put a break point inside the subscription, but the code execution does not stop there.
For testing component, I know there is 'fixture.detectChanges()' method to be applied. For service testing, are there any similar mechanism?
resource.service.ts
#Injectable({
providedIn: 'root',
})
export class ResourceService {
constructor(
private http: HttpClient,
) {}
putData(newData: Data): Observable<Data> {
return this.http.put<Data>('/api/put-endpoint', newData);
}
}
store.service.ts
#Injectable({
providedIn: 'root',
})
export class StoreService {
constructor(
private resource: ResourceService,
private notificationService: NotificationService,
)
saveChanges(newData: Data) {
this.resourceServie.putData(newData).subscribe(() => {
this.notificationService.notifySuccess('Save changes successfully');
// do something else
}
}
store.service.spec.ts
describe('StoreService', () => {
let storeService: StoreService;
let resourceService: jasmine.SpyObj<ResourceService>;
let notificationService: jasmine.SpyObj<NotificationService>;
notificationService = jasmine.createSpyObj('NotificationService', ['notifySuccess']);
resourceService = jasmine.createSpyObj('ResourceService', ['putData']);
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: ResourceService, useValue: resourceService },
{
provide: NotificationService,
useValue: notificationService,
},
],
});
});
it('should saveChanges', () => {
const newData: Data = {foo: bar};
resourceService.putData.and.returnValue(of(newData));
storeService.putData(newData);
expect(resourceService.putData).toHaveBeenCalledWith(newData); // PASS
expect(notificationService.notifySuccess).toHaveBeenCalledWith(Save changes successfully); // FAIL as the code block inside the subscription does not run
});
})

Your spy need to return some value for triggering subscription.
More advanced information about spy you can read here
You need to use:
const newData: Data = {foo: bar};
resourceService = spyOn(resourceService,'putData').and.returnValue(newData);
instead of:
resourceService = jasmine.createSpyObj('ResourceService', ['putData']);
You also can see the similar post - Angular - unit test for a subscribe function in a component
Try to implement store.service.spec.ts in this way:
describe('StoreService', () => {
let storeService: StoreService;
let resourceService: ResourceService;
let notificationService: NotificationService;
const newData: Data = {foo: bar};
resourceService = spyOn(resourceService,'putData').and.returnValue(newData);
resourceService = spyOn(notificationService,'notifySuccess').and.returnValue('Save changes successfully');
beforeEach(() => {
TestBed.configureTestingModule({
providers: [],
});
resourceService= TestBed.inject(ResourceService);
notificationService= TestBed.inject(NotificationService);
storeService = TestBed.inject(storeService);
});
it('should saveChanges', () => {
storeService.saveChanges(newData);
expect(resourceService.putData).toHaveBeenCalledWith(newData);
expect(notificationService.notifySuccess).toHaveBeenCalledWith(Save changes successfully); /
});
})

Related

Testing Service with Mongoose in NestJS

I am trying to test my LoggingService in NestJS and while I cannot see anything that is wrong with the test the error I am getting is Error: Cannot spy the save property because it is not a function; undefined given instead
The function being tested (trimmed for brevity):
#Injectable()
export class LoggingService {
constructor(
#InjectModel(LOGGING_AUTH_MODEL) private readonly loggingAuthModel: Model<IOpenApiAuthLogDocument>,
#InjectModel(LOGGING_EVENT_MODEL) private readonly loggingEventModel: Model<IOpenApiEventLogDocument>,
) {
}
async authLogging(req: Request, requestId: unknown, apiKey: string, statusCode: number, internalMsg: string) {
const authLog: IOpenApiAuthLog = {
///
}
await new this.loggingAuthModel(authLog).save();
}
}
This is pretty much my first NestJS test and as best I can tell this is the correct way to test it, considering the error is right at the end it seems about right.
describe('LoggingService', () => {
let service: LoggingService;
let mockLoggingAuthModel: IOpenApiAuthLogDocument;
let request;
beforeEach(async () => {
request = new JestRequest();
const module: TestingModule = await Test.createTestingModule({
providers: [
LoggingService,
{
provide: getModelToken(LOGGING_AUTH_MODEL),
useValue: MockLoggingAuthModel,
},
{
provide: getModelToken(LOGGING_EVENT_MODEL),
useValue: MockLoggingEventModel,
},
],
}).compile();
service = module.get(LoggingService);
mockLoggingAuthModel = module.get(getModelToken(LOGGING_AUTH_MODEL));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('authLogging', async () => {
const reqId = 'mock-request-id';
const mockApiKey = 'mock-api-key';
const mockStatusCode = 200;
const mockInternalMessage = 'mock-message';
await service.authLogging(request, reqId, mockApiKey, mockStatusCode, mockInternalMessage);
const authSpy = jest.spyOn(mockLoggingAuthModel, 'save');
expect(authSpy).toBeCalled();
});
});
The mock Model:
class MockLoggingAuthModel {
constructor() {
}
public async save(): Promise<void> {
}
}
After much more googling I managed to find this testing examples Repo: https://github.com/jmcdo29/testing-nestjs which includes samples on Mongo and also suggest that using the this.model(data) complicates testing and one should rather use `this.model.create(data).
After making that change the tests are working as expected.
The issue comes from the fact that you pass a class to the TestingModule while telling it that it's a value.
Use useClass to create the TestingModule:
beforeEach(async () => {
request = new JestRequest();
const module: TestingModule = await Test.createTestingModule({
providers: [
LoggingService,
{
provide: getModelToken(LOGGING_AUTH_MODEL),
// Use useClass
useClass: mockLoggingAuthModel,
},
{
provide: getModelToken(LOGGING_EVENT_MODEL),
// Use useClass
useClass: MockLoggingEventModel,
},
],
}).compile();
service = module.get(LoggingService);
mockLoggingAuthModel = module.get(getModelToken(LOGGING_AUTH_MODEL));
});

NestJS accessing private class field before testing method with jest

Assuming there is the following nest service class with the private field myCache and the public method myFunction:
import * as NodeCache from 'node-cache'
class MyService{
private myCache = new NodeCache();
myFunction() {
let data = this.myCache.get('data');
if(data === undefined){
// get data with an http request and store it in this.myCache with the key 'data'
}
return data;
}
}
I want to test the function myFunction for two different cases.
Fist case: If condition is true. Second Case: If condition is false.
Here is the test class with the two missing tests:
import { Test, TestingModule } from '#nestjs/testing';
import { MyService} from './myService';
describe('MyService', () => {
let service: MyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MyService],
}).compile();
service = module.get<MyService>(MyService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('myFunction', () => {
it('should return chached data', () => {
// first test
}),
it('should return new mocked data', () => {
// second test
})
})
});
Therefore I guess I have to access or mock the myCache private class field.
Because it is private I can't access it in the test class.
My Question is: What's the best and correct way to achieve this?
If you're just looking to mock it, you can always use as any to tell Typescript to not warn you about accessing private values.
jest.spyOn((service as any).myCache, 'get').mockReturnValueOnce(someValue);
However, that's kind of annoying to have to do over and over again and not really the best practice. What I would do instead is move your cache to be an injectable provider so that it could be swapped out at a moments notice and your MyService no longer has a hard dependency on node-cache. Something like this:
// my.module.ts
#Module({
providers: [
MyService,
{
provide: 'CACHE',
useClass: NodeCache
}
]
})
export class MyModule {}
// my.service.ts
#Injectable()
export class MyService {
constructor(#Inject('CACHE') private readonly myCache: NodeCache) {}
...
And now in your test you can swap out the CACHE token for a mock implementation that can also be retrieved in your beforeEach block, meaning no more any.
describe('MyService', () => {
let service: MyService;
let cache: { get; set; }; // you can change the type here
beforeEach(async () => {
const modRef = await Test.createTestingModule({
providers: [
MyService,
{
provide: 'CACHE',
useValue: { get: jest.fn(), set: jest.fn() }
}
]
}).compile();
service = modRef.get(MyService);
cache = modRef.get<{ get; set; }>('CACHE');
});
});
And now you can call jest.spyOn(cache, 'get') without the use of as any.

Observable "source is deprecated" error on npm lint

I get "source is deprecated: This is an internal implementation detail, do not use." when I run the command npm lint on my code below:
set stream(source: Observable<any>) {
this.source = source;
}
If I take it out, it satisfies the lint, but it breaks my unit tests. Why is this?
If you are testing effects, you need to update the approach. I have changed using the provideMockActions, the action would be an let actions$: Observable;
fdescribe('PizzaEffects', () => {
let actions$: Observable;;
let service: Service;
let effects: PizzaEffects;
const data = givenPizzaData();
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApolloTestingModule],
providers: [
Service,
PizzaEffects,
Apollo,
// { provide: Actions, useFactory: getActions }, remove
provideMockActions(() => actions$),
]
});
actions$ = TestBed.get(Actions);
service = TestBed.get(Service);
effects = TestBed.get(PizzaEffects);
spyOn(service, 'loadData').and.returnValue(of(data));
});
describe('loadPizza', () => {
it('should return a collection from LoadPizzaSuccess', () => {
const action = new TriggerAction();
const completion = new LoadPizzaSuccess(data);
actions$ = hot('-a', { a: action });
const expected = cold('-b', { b: completion });
expect(effects.getPizzaEffect$).toBeObservable(expected);
});
});
});

Angular - get synchronously from Promise

I want to print history of products. I have an id of product in ActivatedRoute.params. In ngOnInit method I have to get all history of product and assign to variable. Then I want to map product to productHistory, because I want to have last version with history toghether. But the problem is with getting history. Method to getting history return Promise and I cannot get length of productsHistory when I use this property and I get undefined. How can I get this property after loading from service?
I want to execute method after execution getHistory().
My code:
ProductService.ts:
import { Injectable } from '#angular/core';
import { Headers, Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
// rest imports
#Injectable()
export class ProductService {
// URL to web api
private projectsUrl = 'http://localhost:8080/products';
private headers = new Headers({'Content-Type': 'application/json'});
constructor(private http: Http) {}
getHistory(id: number): Promise<ProductHistory[]> {
const url = `${this.projectsUrl}/projectId/${id}`;
return this.http.get(url)
.toPromise()
.then(response => response.json() as ProductHistory[])
.catch(this.handleError);
}
handleError() {
//...
// implementation is irrelevant
}
}
ProductHistoryComponent.ts:
import { Component, Input, OnInit } from '#angular/core';
import { ActivatedRoute, Params } from '#angular/router';
import { Location } from '#angular/common';
import { ProductService } from './product.service';
import { ProductHistory } from './product-history';
import { Product } from './model/product';
import 'rxjs/add/operator/switchMap';
#Component({
selector: 'product-history',
templateUrl: './product-history.component.html',
styleUrls: [ './product-history.component.css' ]
})
export class ProductHistoryComponent implements OnInit {
auditProducts: ProductHistory[] = new Array<ProductHistory[]>();
selectedProduct: ProductHistory;
constructor(
private route: ActivatedRoute,
private location: Location,
private productService: ProductService
) {}
ngOnInit(): void {
let id: number = this.route.snapshot.params['id'];
this.productService.getHistory(id)
.then(history => this.historyProducts = history);
this.productService.getProduct(id)
.then(product => {
let lastVersion: ProductHistory = this.createLastVersion(product);
this.auditProducts.push(lastVersion);
});
}
onSelect(ProductHistory: ProductHistory): void {
this.selectedProduct = ProductHistory;
this.compare(this.selectedProduct);
}
goBack(): void {
this.location.back();
}
compare(history: ProductHistory): void {
let previous: ProductHistory;
if (history.changeNumber != null && history.changeNumber > 1) {
previous = this.historyProducts[history.changeNumber - 2];
if (typeof previous != 'undefined') {
this.setPreviousDiffsFalse(previous);
if (previous.name !== history.name) {
history.nameDiff = true;
}
if (previous.price !== history.price) {
history.priceDiff = true;
}
}
}
}
createLastVersion(product: Product): ProductHistory {
let lastVersionProduct: ProductHistory = new ProductHistory();
lastVersionProduct.id = this.historyProducts.length + 1;
lastVersionProduct.name = product.name;
lastVersionProduct.price = product.price;
lastVersionProduct.changeNumber = this.historyProducts[this.historyProducts.length - 1].changeNumber + 1;
return lastVersionProduct;
}
setPreviousDiffsFalse(previous: ProductHistory): void {
previous.nameDiff = false;
previous.priceDiff = false;
}
}
You can't run it synchronously, you have to wait for each promise to return a result before you can do something with that result. The normal way to do this is to nest code inside then blocks when using promises. Alternatively you can also use async/await with the latest version of typescript and you only have to change your component code as you are already returning the Promise type from your service. This makes code easier to read (IMO) although the emitted javascript code will still use function/callback nesting (unless you are targeting es7 I believe, maybe someone will correct or confirm this).
// note the use of async and await which gives the appearance of synchronous execution
async ngOnInit() {
let id: number = this.route.snapshot.params['id'];
const history = await this.productService.getHistory(id);
this.historyProducts = history;
const product = await this.productService.getProduct(id);
let lastVersion: ProductHistory = this.createLastVersion(product);
this.auditProducts.push(lastVersion);
}
I would suggest using observables instead of promises ... but to answer your question, you just need to perform the second request after the first is received. Something like this:
ngOnInit(): void {
let id: number = this.route.snapshot.params['id'];
this.productService.getHistory(id)
.then(history => {
this.historyProducts = history);
this.productService.getProduct(id)
.then(product => {
let lastVersion: ProductHistory = this.createLastVersion(product);
this.auditProducts.push(lastVersion);
});
}
}
I just moved the second request within the then of the first request. NOTE: I did not syntax check this.

Array undefined on map and subscribe Ionic 2

I creted a service that I want to get data from a JSON file and assign to an array (countries) to use at entire application (a lots of Pages), but when I call getCountries method, the countries is undefined, what is wrong at my approach?
import { Http } from '#angular/http';
import { Injectable } from '#angular/core';
#Injectable()
export class CountryService {
private countries: any;
private isLoaded: boolean;
private url: string = 'http://localhost:8100/assets/data/countriesV2.json';
constructor(private http: Http) {
if (!this.isLoaded) {
this.http.get(this.url)
.map(res => res.json())
.subscribe(result => {
this.countries = result;
});
this.isLoaded = true;
}
}
public getCountries() {
console.log(this.countries);
return this.countries();
}
}
Maybe changing return this.countries(); to return this.countries; may help
Also, check that your result is not empty :
.subscribe(result => {
this.countries = result;
console.log(result)
});
You should always map the data in the service, and subscribe in your component. The reason why this.countries is undefined because it IS undefined, even though you are trying to do the request in the constructor, it's not going to work. Change your service to this:
#Injectable()
export class CountryService {
private url: string = 'http://localhost:8100/assets/data/countriesV2.json';
constructor(private http: Http) { }
public getCountries() {
this.http.get(this.url)
.map(res => res.json())
}
}
And then in your components you call getCountries and subscribe to the request.
countries: any[] = [];
constructor(private countryService: CountryService) { }
ngOnInit() {
this.countryService.getCountries()
.subscribe(data => {
this.countries = data;
// here countries is not undefined, so call your "randomCountry" method here!
this.getRandomCountry();
});
}
Since this is an async operation, I suggest you use the safe navigation operator in that view, that does not try to show the property of country in case country is null. More info here. So the usage would be:
<div *ngFor="let country of countries">
{{country?.yourProperty}} // replace "yourProperty" with your actual property
</div>
Some more detailed explanations about HTTP here from the official docs
Hope this helps! :)

Categories

Resources