I have a very common/simple scenario. I have an http service which makes a call to local json files and get data which will ultimately be replaced by actual api calls.
I want to mock the service and write test on ngOnInit of Component but it seems its not working.
factory-form.component.ts
import { Component, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
import { Factory } from './factory';
import { FactoryService } from './factory.service';
#Component({
moduleId: module.id,
selector: 'factory-form',
templateUrl: './factory-form.component.html',
styleUrls: ['./factory-form.component.css'],
providers: [FactoryService]
})
export class FactoryFormComponent implements OnInit {
private model: Factory = new Factory();
countries;
factoryStatuses;
productTypes;
risks;
private errorMessage: string;
private submitted: boolean = false;
private active: boolean = true;
constructor(private factoryService: FactoryService) {
}
ngOnInit(): void {
this.getCountries();
}
private getCountries() {
this.factoryService.getCountry()
.subscribe(countries => this.countries = countries,
error => this.errorMessage = error);
}
onSubmit(): void {
this.submitted = true;
this.factoryService.saveFactory(this.model);
}
}
factory.service.ts
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Factory } from './factory';
import { IDictionary } from '../shared/dictionary';
import { AppSettings } from '../shared/app.constants';
#Injectable()
export class FactoryService {
constructor(private http: Http) {
}
getCountry(): Observable<IDictionary[]> {
return this.http.get(AppSettings.countryUrl)
.map(this.extractData)
.do(data => console.log("get Countries: " + JSON.stringify(data)))
.catch(this.handleError);
}
private extractData(response: Response) {
let body = response.json();
return body || {};
}
private handleError(error: Response) {
console.log(error);
return Observable.throw(error.json().error || "500 internal server error");
}
}
factory.service.mock.ts
import {provide, Provider} from '#angular/core';
import {FactoryService} from './factory.service';
import * as Rx from 'rxjs/Rx';
export class MockFactoryService extends FactoryService{
public fakeResponse: any = [{"id": 1, "name": "uk"}];
public getCountry(): Rx.Observable<any> {
let subject = new Rx.ReplaySubject()
subject.next(this.fakeResponse);
return subject;
}
public setResponse(response: any): void {
this.fakeResponse = response;
}
public getProvider(): Provider {
return provide(FactoryService, { useValue: this });
}
}
factory-form.component.spec.ts
/* tslint:disable:no-unused-varfoiable */
import { By } from '#angular/platform-browser';
import { DebugElement, provide } from '#angular/core';
import {
beforeEach, beforeEachProviders,
describe, xdescribe,
expect, it, xit,
async, inject,
addProviders,
fakeAsync,
tick,
ComponentFixture,
TestComponentBuilder
} from '#angular/core/testing';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { FactoryService } from './factory.service';
import { MockFactoryService } from './factory.service.mock';
import { FactoryFormComponent } from './factory-form.component';
beforeEachProviders(() => [FactoryFormComponent, provide(FactoryService, { useClass: MockFactoryService })]);
describe('When loading the FactoryFormComponent', () => {
var builder;
beforeEach(inject([TestComponentBuilder], (tcb) => {
builder = tcb;
builder.overrideProviders(FactoryFormComponent,
[
{ provide: FactoryService, useClass: MockFactoryService }
])
.overrideTemplate('<h1>fake tempalte</h1>');
}));
it('should call ngOnInit', async(() => {
let component = builder.createAsync(FactoryFormComponent);
console.log("component?" + JSON.stringify(component));
component.then((fixture: ComponentFixture<FactoryFormComponent>) => {
console.log("inside block");
fixture.detectChanges();
var compiled = fixture.debugElement.nativeElement;
expect(fixture.componentInstance.ngOnInit).toHaveBeenCalled();
expect(fixture.componentInstance.MockFactoryService.getCountry).toHaveBeenCalled();
}).catch((error) => {
console.log("error occured: " + error);
});;
}));
});
this is the output
12 07 2016 16:54:56.314:INFO [watcher]: Changed file "C:/Projects/ethical_resourcing/src/Ethos.Client/dist/app/factory/factory-form.component.spec.js".
12 07 2016 16:54:56.393:INFO [watcher]: Changed file "C:/Projects/ethical_resourcing/src/Ethos.Client/dist/app/factory/factory-form.component.spec.js.map".
12 07 2016 16:54:56.988:WARN [web-server]: 404: /base/dist/vendor/systemjs/dist/system-polyfills.js.map
LOG: 'component?{"_handler":{"resolved":false}}'
Chrome 51.0.2704 (Windows 7 0.0.0): Executed 1 of 1 SUCCESS (0.108 secs / 0.1 secs)
for some reason its passing because its not going to the inner block. I deliberately broke the creatAsync.then() code to see what is coming as component value.
If someone has a better suggestion to mock the service that would be great.
Update: I did try with templateUrl and overriding it but no luck. maybe missing some fundamental thing.
update 2: I am using angular-cli and just call ng test.
Related
I have a simple service:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { ApiConfiguration } from '../../api/api-configuration';
import { BaseService } from '../../api/base-service';
import { Observable } from 'rxjs';
const TIMER = 120000;
#Injectable({
providedIn: 'root'
})
export class AppService extends BaseService {
constructor(
config: ApiConfiguration,
http?: HttpClient,
) {
super(config, http);
}
getContextualHelp(id, helpType): Observable<Object> {
return this.http.get(`${this.rootUrl}/api/v2/contextual-help`, {
params: { id, helpType }
});
}
}
I have tests that make sure the service returns some mocked data:
import { AppService } from '../app.service';
import { TestBed } from '#angular/core/testing';
import { HttpTestingController, HttpClientTestingModule } from '#angular/common/http/testing';
import { StoreModule } from '#ngrx/store';
import { ApiConfiguration } from '../../../api/api-configuration';
fdescribe('app.service', () => {
let service: AppService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
StoreModule.forRoot({}),
],
providers: [
AppService,
{
provide: ApiConfiguration, useValue: ''
},
]
});
service = TestBed.get(AppService);
httpMock = TestBed.get(HttpTestingController);
});
describe('getContextualHelp', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});
it('calls the contextual help route with the expected params', () => {
const mockData = '<div></div>';
service.rootUrl = 'https://www.mock.com';
service.getContextualHelp('foo', 'bar').subscribe((html) => {
expect(html).toEqual('<div></div>');
});
const req = httpMock.expectOne('https://www.mock.com/api/v2/contextual-help');
req.flush(mockData);
});
});
});
It seems like I have everything hooked up correctly but when I run the test I get:
Error: Expected one matching request for criteria "Match URL: https://www.mock.com/api/v2/contextual-help", found none.
When I debug the test and go into the code context I see that http client is being called, and it's being called with the url I expect. So I guess somehow I misconfigured the test modules somehow? Does anyone see what I might have done wrong?
Note that expectOne() matches the url as well as the url params. Try to pass the complete url to the function, like:
const req = httpMock.expectOne('https://www.mock.com/api/v2/contextual-help?id=foo&helpType=bar');
Take a look at this question and the answer.
api-connector.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpParams } from '#angular/common/http';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Observable } from 'rxjs/Observable';
import {environment} from '../../../environments/environment';
import { catchError } from 'rxjs/operators/catchError';
#Injectable()
export class ApiConnectorService {
constructor(private http: HttpClient) { }
private getQueryString(params): string {
const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&');
console.log('QUERY STRING', queryString);
return ('?' + queryString);
}
private formatErrors(error: any) {
return new ErrorObservable(error.error);
}
get(path: string, payload: Object = {}): Observable<any> {
return this.http.get(`${environment.base_url}${path}` + this.getQueryString(payload))
.pipe(catchError(this.formatErrors));
}
put(path: string, body: Object = {}): Observable<any> {
return this.http.put(
`${environment.base_url}${path}`,
body
).pipe(catchError(this.formatErrors));
}
post(path: string, body: Object): Observable<any> {
// console.log('API SERVICE BODY', body)
return this.http.post(
`${environment.base_url}${path}`,
body
).pipe(catchError(this.formatErrors));
}
delete(path): Observable<any> {
return this.http.delete(
`${environment.base_url}${path}`
).pipe(catchError(this.formatErrors));
}
}
login.contract.ts
export interface LoginRequest {
env?: string;
userid: string;
password: string;
newpassword: string;
}
export interface LoginResponse {
token: string;
}
I am pretty new to Angular and as well Karma/Jasmine also.
I have created a simple login component and login service. While writing test cases for that purpose, I followed some docs and angular.io site. I have written some of the test cases for login component with help of docs, but I didn't manage to write test cases for login service.
How to write test cases for login service?
Here is my login.service.ts file
import { Injectable } from '#angular/core';
import { ApiConnectorService } from '../api-handlers/api-connector.service';
import { LoginRequest, LoginResponse } from './login.contract';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
#Injectable()
export class LoginService {
constructor(private apiConnector: ApiConnectorService) { }
login(payload: LoginRequest): Observable<LoginResponse> {
console.log('Login payload ', payload);
return this.apiConnector.post('/api/login', payload)
.pipe(
map((data: LoginResponse) => data)
)
}
}
Having had a think about it this is how I would approach testing your service. I can't do the exact details for the last test as I don't have details on your ApiConnectorService or LoginResponse object but I'm sure you'll get the idea.
import { TestBed, inject } from '#angular/core/testing';
import { LoginService } from './login.service';
import { LoginResponse, LoginRequest } from './login.contract';
import { Observable, of } from 'rxjs';
import { ApiConnectorService } from './api-connector.service';
class ApiConnectorServiceStub {
constructor() { }
post(address: string, payload: LoginRequest): Observable<LoginResponse> {
return of(new LoginResponse());
}
}
describe('LoginService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LoginService,
{provide: ApiConnectorService, useClass: ApiConnectorServiceStub }]
});
});
it('should be created', inject([LoginService], (service: LoginService) => {
expect(service).toBeTruthy();
}));
it('should call post on apiConnectorService with right parameters when login is called',
inject([LoginService], (service: LoginService) => {
const apiConnectorStub = TestBed.get(ApiConnectorService);
const spy = spyOn(apiConnectorStub, 'post').and.returnValue(of(new LoginResponse()));
const loginRequest = of(new LoginRequest());
service.login(loginRequest);
expect(spy).toHaveBeenCalledWith('/api/login', loginRequest);
}));
it('should map data correctly when login is called', inject([LoginService], (service: LoginService) => {
const apiConnectorStub = TestBed.get(ApiConnectorService);
// Set you apiConnector output data here
const apiData = of('Test Data');
const spy = spyOn(apiConnectorStub, 'post').and.returnValue(apiData);
const result = service.login(of(new LoginRequest()));
// Set your expected LoginResponse here.
const expextedResult = of(new LoginResponse());
expect(result).toEqual(expextedResult);
}));
});
I'm working on a project, and have come to a huge blocking point.
As I mentioned in my question, I've built a dataService, but my dataService shows the response properly, but it comes up as undefined in my Component.
Here's my code for the data.service.ts file
import { Injectable, OnInit } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import {
Http,
Response,
Request,
RequestOptions,
Headers
} from '#angular/http';
import {Observable} from 'rxjs/Observable';
import { Institution } from './institution';
import 'rxjs/Rx';
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
#Injectable()
export class DataService {
constructor(private http: HttpClient) { }
institutionData: Object;
//private http: Http;
grabInstitutionData(): Observable<Institution[]> {
return this.http
.get(`http://127.0.0.1:8000/courses/api/institution/list/`)
.map((response: Response) => {
console.log(response);
this.institutionData = <Institution[]>response.json();
console.log('this is right' + this.institutionData);
return this.institutionData;
})
.catch(this.handleError);
}
private handleError(error: Response) {
return Observable.throw(error.statusText);
}
}
And my code for the Component File:
import { async } from '#angular/core/testing';
import { Component, OnInit } from '#angular/core';
import { DataService } from '../data.service';
import {Http, Response} from '#angular/http';
import { HttpClient } from '#angular/common/http';
import { Institution } from '../institution';
#Component({
selector: 'app-institutions',
templateUrl: './institutions.component.html',
styleUrls: ['./institutions.component.css']
})
export class InstitutionsComponent implements OnInit {
institutionData: Object;
_InstitutionsArray: Institution[];
constructor(private http: Http, private dataService: DataService) { }
getInstitutions(): void {
this.dataService.grabInstitutionData()
.subscribe(
resultArray => this._InstitutionsArray = resultArray,
error => console.log("Error :: " + error)
);
}
ngOnInit(): void {
this.getInstitutions();
}
}
I know its related to something pertaining to the asynchronous function call, but I can't exactly figure out what it would be.
Any and all help would be appreciated.
El-Teezus, not use map and not use json(). When we use "map" is for transform the response, It have not sense in your code
//In our service
grabInstitutionData(): Observable<Institution[]> {
return this.http
.get(`http://127.0.0.1:8000/courses/api/institution/list/`)
.do(response:Response)=>{
console.log(response);
this.institutionData = <Institution[]>response;
console.log('this is right' + this.institutionData);
})
.catch(this.handleError);
}
See that we use "do" to do "something" with the response without change it. Yes, "do" is to check if a respone is the respone expected or not or to cache the result anyway.
See too that we don't need write response.json(). HttpClient make it for us.
In your component
getInstitutions(): void {
this.dataService.grabInstitutionData()
.subscribe(
(resultArray) =>
{
this._InstitutionsArray = resultArray
//here you have the data
console.log(this._Institutionsrray);
},
(error) => console.log("Error :: " + error)
);
}
I'm trying to display a list with data fetched from a local json file. Here is how my service looks so far.
category.service.ts
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
#Injectable()
export class CategoryService {
constructor(private _http: Http) { }
getCategories() {
return this._http.get('api/categories.json')
.map((response: Response) => response.json())
.do(data => console.log('All; ' + JSON.stringify(data)))
.catch(this.handleError);
}
private handleError(error: Response) {
console.log(error);
return Observable.throw(error.json().error || 'Server error');
}
}
here is how the Category Menu component where I inject it looks so far
import { Component, OnInit } from '#angular/core';
import { CategoryService } from '../category.service';
#Component({
selector: 'app-category-menu',
templateUrl: './category-menu.component.html',
styleUrls: ['./category-menu.component.css']
})
export class CategoryMenuComponent implements OnInit {
errorMessage: string;
commerceName= `El baratón`;
categoryName = `Bebidas`;
categories: any = 'Not assigned yet';
hasMenu?= true;
constructor(private _categoryService: CategoryService) { }
ngOnInit() {
return this._categoryService.getCategories()
.subscribe(
categories => this.categories = categories,
error => this.errorMessage = error
);
}
}
Now, the error I am getting in the console is a 404. For this project I have used the CLI version 1.0 and the categories.json file inside of the src/api/categories.json folder.
What am I doing wrong here?
move your api/categories.json to assets folder, then change url to
return this._http.get('/assets/api/categories.json')
I am a beginner to ionic 2 unit testing. I followed angular 2 documentation (https://angular.io/docs/ts/latest/guide/testing.html) to test my ionic 2 application with karma and jasmine.
But now I am stuck in an error called
'Cannot read property '_getPortal' of undefined'
here is my LocationSearchModal.ts file
import { Component } from '#angular/core';
import { NavController, ViewController } from 'ionic-angular';
import { Location } from '../../services/domain/Location';
import { LocationService } from '../../services/LocationService';
import { LoadingController } from 'ionic-angular';
#Component({
selector: 'location-search-modal',
templateUrl: 'location-search-modal.html'
})
export class LocationSearchModal {
locationList: Array<Location> = new Array<Location>();
selectedLocation: number;
temp: any = "test";
constructor(public navCtrl: NavController, public locationService: LocationService, public viewController: ViewController, public loadingController: LoadingController) {
this.filterLocationsForString();
}
filterLocations(event: any): void {
const searchString: string = event.target.value;
this.filterLocationsForString(searchString);
console.log(this.filterLocationsForString(searchString));
}
filterLocationsForString(searchString?: string) {
let loader = this.loadingController.create({
content: "loading"
});
loader.present();
this.locationService.getLocationsForLikeSearchString(searchString)
.subscribe((result) => {
loader.dismissAll();
this.locationList = result
});
console.log(this.locationList);
}
closeLocationSearch() {
this.locationService.getLocationById(this.selectedLocation)
.subscribe((location) => this.viewController.dismiss(location[0]));
}
}
and I used service called locationService.ts there and this is that service
import { Injectable } from '#angular/core';
import { Location } from './domain/Location';
import { DatabaseAccessor } from '../database/DatabaseAccessor';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class LocationService {
locationList:Array<Location> = new Array<Location>();
constructor(public databaseAccessor: DatabaseAccessor) {}
getLocationsForLikeSearchString(searchString: string) : Observable<Array<Location>> {
const searchValue = (searchString == null) ? '%' : searchString.trim() + '%';
return <Observable<Array<Location>>> Observable.fromPromise(this.databaseAccessor.runSelectQuery(Location, new Location(), 'WHERE name LIKE ?', [searchValue]));
}
getLocationById(id: number): Observable<Location> {
return <Observable<Location>> Observable.fromPromise(this.databaseAccessor.runSelectQuery(Location, new Location(), 'WHERE id = ?', [id]));
}
saveLocations(locations: Array<Location>){
this.databaseAccessor.runInsertBatchQuery(Location.prototype, locations);
}
}
Finally, I wrote a spec.ts file to unit testing and here is that,
import { ComponentFixture, async } from '#angular/core/testing';
import { LocationSearchModal } from './LocationSearchModal';
import { LocationService } from '../../services/LocationService';
import { TestUtils } from '../../test';
import { TestBed } from '#angular/core/testing';
import { App, NavController, Platform, Config, Keyboard, Form, IonicModule, GestureController, ViewController, LoadingController } from 'ionic-angular';
import { ConfigMock } from '../../mocks';
import { TranslateModule } from 'ng2-translate';
import { DatabaseAccessor } from '../../database/DatabaseAccessor';
let comp: LocationSearchModal;
let fixture: ComponentFixture<LocationSearchModal>;
let instance: any = null;
describe('LocationSearchModal', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LocationSearchModal], // declare the test component
providers: [App, Platform, Form, Keyboard, NavController, GestureController, LoadingController, LocationService, DatabaseAccessor,
{ provide: ViewController, useClass: class { ViewController = jasmine.createSpy("viewController"); } },
{ provide: Config, useClass: ConfigMock },
],
imports: [
IonicModule,
TranslateModule.forRoot(),
],
});
fixture = TestBed.createComponent(LocationSearchModal);
comp = fixture.componentInstance;
}));
console.log(comp);
it('Testing Location Component', () => {
expect(comp.temp).toBe('test');
})
});
when I am running the following error comes from the terminal.
(my unit testing configuration are correct and I tested it with another simple .spec.ts file)
the error
SUMMARY:
✔ 1 test completed
✖ 1 test failed
FAILED TESTS:
LocationSearchModal
✖ Testing Location Component
Chrome 54.0.2840 (Linux 0.0.0)
Failed: Error in ./LocationSearchModal class LocationSearchModal_Host - inline template:0:0 caused by: Cannot read property '_getPortal' of undefined
TypeError: Cannot read property '_getPortal' of undefined
at App.present (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/~/ionic-angular/components/app/app.js:78:0 <- src/test.ts:2091:35)
at Loading.present (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/~/ionic-angular/components/loading/loading.js:31:0 <- src/test.ts:38779:26)
at LocationSearchModal.filterLocationsForString (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/src/pages/location-search/LocationSearchModal.ts:9:4184 <- src/test.ts:18993:4170)
at new LocationSearchModal (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/src/pages/location-search/LocationSearchModal.ts:9:3407 <- src/test.ts:18993:3391)
at new Wrapper_LocationSearchModal (/DynamicTestModule/LocationSearchModal/wrapper.ngfactory.js:7:18)
at _View_LocationSearchModal_Host0.createInternal (/DynamicTestModule/LocationSearchModal/host.ngfactory.js:16:35)
at _View_LocationSearchModal_Host0.AppView.create (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/~/#angular/core/src/linker/view.js:84:0 <- src/test.ts:52350:21)
at _View_LocationSearchModal_Host0.DebugAppView.create (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/~/#angular/core/src/linker/view.js:294:0 <- src/test.ts:52560:44)
at ComponentFactory.create (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/~/#angular/core/src/linker/component_factory.js:152:0 <- src/test.ts:32035:36)
at initComponent (webpack:/media/dilanka/Stuff/CODE%20BASE/Inspection/Unit%20Testing/Inspection-Rewrite/~/#angular/core/bundles/core-testing.umd.js:855:0 <- src/test.ts:7416:53)
Mock the LoadingController if the problem is from using LoadingController
export class LoadingControllerMock {
_getPortal(): any { return {} };
create(options?: any) {
return new LoadingMock()
};
}
class LoadingMock {
present() { };
dismiss() { };
dismissAll() { };
}
Import the Mock and the actual from wherever
import { LoadingController } from 'ionic-angular';
import { LoadingControllerMock } from '../../../../test-config/mocks-ionic';
Substitute
providers: [
{ provide: LoadingController, useClass: LoadingControllerMock }
]
I Solved this problem finally. I used a mock and defined required methods in that mock. Then It works :)
here is an example for a mock.
export class ViewControllerMock {
public _setHeader(): any { return {} };
public _setNavbar(): any { return {} };
public _setIONContent(): any { return {} };
public _setIONContentRef(): any { return {} };
}
then have to import that mock into your .spec.ts file as follows
import {ViewControllerMock} from '../../mocks';
then have to define that mock in your providers in spec.ts file as follows
providers: [{ provide: ViewController, useClass: ViewControllerMock}],