I am very new to writing tests in Karma and Jasmine. In my case, I have a dynamic configuration file that loads before the app is initialized and that file is a JSON with a value.
configuration.json
{
"sampleConfigValue": "this is a sample value from config"
}
Configuration.ts
export interface Configuration {
sampleConfigValue: string;
}
ConfigurationService.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Configuration } from './configuration';
#Injectable({
providedIn: 'root'
})
export class ConfigurationService {
private configData: any | undefined;
private readonly configPath: string = '../assets/demo/data/config.json';
constructor(
private http: HttpClient
) { }
async loadConfiguration(): Promise<any> {
try {
const response = await this.http.get(`${this.configPath}`)
.toPromise().then(res => this.configData = res);
return this.configData;
} catch (err) {
return Promise.reject(err);
}
}
get config(): Configuration | undefined {
return this.configData;
}
}
Exporting the ConfigurationLoader in app.module.ts
export function configLoader(injector: Injector) : () => Promise<any>
{
return () => injector.get(ConfigurationService).loadConfiguration();
}
and Provider in app.module.ts
{provide: APP_INITIALIZER, useFactory: configLoader, deps: [Injector], multi: true},
configuration.service.spec.ts
import { TestBed } from '#angular/core/testing';
import { ConfigurationService } from './configuration.service';
describe('ConfigurationService', () => {
let service: ConfigurationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ConfigurationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
The configuration file is working but I am wondering how to write a test case for this dynamic configuration in my project?
Your time and help will really help me :)
Thanks :)
When unit testing, you're supposed to test a code unit and mock the rest.
So create a mock then test :
// Put this in the main describe
const returnValue = {};
let httpMock: { get: jasmine.Spy };
let service: ConfigurationService;
// Put this in the main beforeEach
httpMock = {
get: jasmine.createSpy().and.returnValue(of(returnValue)),
};
service = new ConfigurationService(<any>httpMock);
// Make a meaningful test
it('Should call the endpoint and retrieve the config', (done) => {
service.loadConfiguration().then(() => {
expect(httpMock.get)
.toHaveBeenCalledOnceWith(service['configPath']);
expect(service['configData']).toBe(returnValue);
done();
});
});
Here is my component code. In this code I have stored all data in a local array to find an item from this array. But when I try to get an element from this array it shows undefined.
//-------------------------------------------------------------
Component.ts
export class AccountsComponent implements OnInit
{
retVal = [];
constructor(
public service:AccountingService)
{
this.service.getAccounts().forEach(item=>{
this.retVal.push(item['chartofaccount']); // Locally stored the value to an array//
});
}
ngOnInit()
{
console.log(this.getAccountById(2));
}
getAccountById(id)
{
return this.retVal.find(x => x.id === id); // Return value showed undefined//
}
} //-------------------------------------------------------------
Service.ts
getAccounts():Observable<ChartOfAccount[]>
{
return this._htc.get<ChartOfAccount[]>(this.apiUrl+'chart-of-account', httpOptions)
.pipe(
tap(data => console.log("Data:", data)),
);
}
Try to call your service methods in new method in your component instead of constructor.
This approach should fix your problem.
Why?
Angular: function calling on constructor
https://www.angularjswiki.com/angular/what-is-the-difference-between-constructor-and-ngoninit-in-angular/
//-------------------------------------------------------------
Component.ts
export class AccountsComponent implements OnInit
{
retVal = [];
constructor(
public service:AccountingService)
{
});
}
ngOnInit()
{ this.getAccountsData();
console.log(this.getAccountById(2));
}
getAccountsData() {
this.service.getAccounts().forEach(item=>{
this.retVal.push(item['chartofaccount']); // Locally stored the value to an array//
});
}
getAccountById(id)
{
return this.retVal.find(x => x.id === id); // Return value showed undefined//
}
} //-------------------------------------------------------------
I have a server running on "localhost:3000". It displays data as JSON at e.g. "localhost:300/locations".
My "data.service.ts" includes this code:
path: string = 'http://localhost:3000'
constructor(private http: HttpClient) { }
// Locations
getAllLocations(): Observable<Location[]> {
let location = null;
this.http.get(this.path + '/locations')
.map((res => location = res))
.catch((error: any) => Observable.throw(console.log(error)));
return location;
}
In my result.component.ts I'm running this code:
constuctor(private dataservice: DataService) { }
ngOnInit() {
console.info(this.dataservice.getAllLocations());
}
I'm expecting to get as output all Locations as JSON, instead of this the output is "null".
Does anyone have a suggestion on how to make this work properly?
UPDATE:
Also tried this for the HTTP call:
getAllLocations(): Observable<Location[]> {
this.http.get<Location[]>(this.path + '/locations')
.pipe(
tap(items => console.info('fetched items'))
);
}
The output for this code is unfortunately: "Object { _isScalar: false, source: {...}, operator: {...} }"
Did you know that HttpClient#get returns an Observable? You can just return the get method in your method.
Secondly, you can set an interface to the method so that it'll return the JSON as typed.
Lastly, you can use template literals in your API URL.
/**
* Retrieves a list of locations.
* (TODO: Add better documentation here)
*/
getAllLocations(): Observable<Location[]> {
return this.http.get<Location[]>(`${this.path}/locations`);
}
You can then handle this in the actual component that calls this method:
constuctor(private dataservice: DataService) { }
ngOnInit() {
this.dataservice.getAllLocations().subscribe(result => {
console.log(result);
});
}
You have to return Observable from the service:
path: string = 'http://localhost:3000'
constructor(private http: HttpClient) { }
// Locations
getAllLocations(): Observable<Locations[]> {
return this.http.get(this.path + '/locations').pipe(
map((res => location = res)),
catch((error: any) => Observable.throw(console.log(error))));
}
And subscribe to it in the component.
constructor(private dataservice: DataService) { }
ngOnInit() {
this.dataservice.getAllLocations().subscribe(result => {
console.log(result);
})
}
I am trying to get a data from json file in the assets folder, and then assign this data to a variable that will be binded to another #Input variable of a child componenet.
Code
Based on multiple solutions on the net, I retrieve my JSON data this way:
#Injectable()
export class JSONService {
constructor(private http: HttpClient) { }
public fromJSON(jsonFileName: string): Observable<any[]> {
let result: any[] = new Array();
let pathToJson: string = "assets/" + jsonFileName + ".json";
return this.http.get(pathToJson).map(data => {
let result: any[] = new Array();
// Apply some treatment on data and push it to the result array
return result;
});
}
}
I then call my service in the ngOnInit() method of the parent component:
ngOnInit() {
this.jsonService.fromJSON("users.json").subscribe(fields => {
this.fields= fields;
console.log(this.fields); // Log (I): this.fields is well defined
});
console.log(this.fields); // Log (II): this.fields is undefined
}
Where the variable fields is binded to a child component:
<child-component [childFields] = "fields"></child-component>
Problem
The problem that I am facing is that the asynchronous call to the fromJSON method causes this.fields to be undefined at some point of the lifecycle of the page execution (Log (II) from the code above), and this causes to send an undefined value of the this.fields variable to the child component.
How to avoid to have an undefined value of the fields variable, and make sure that the child component is always loaded with the data from the json file?
Just add *ngIf to check if the data is loaded
<child-component *ngIf="fields" [childFields] = "fields"></child-component>
Service.ts
#Injectable()
export class JSONService {
constructor(private http: HttpClient) { }
public fromJSON(jsonFileName): Observable<any[]> {
console.warn('Retriving Default Data from File.......');
return this.http.get(filename)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body || [];
}
private handleError(error: any) {
const errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg);
console.log('Server Error!');
return Observable.throw(errMsg);
}
}
parent.component.ts
constructor(public jsonService: jsonService) {
}
ngOnInit() {
this.jsonService.fromJSON('assets/users.json').subscribe(
function (success) {
this.data = success;
this.datahandle(success);
},
error => console.log('Getting Server Data Error :: ' +
JSON.stringify(error)));
}
datahandle(jsonData){
console.log('check data' + JSON.stringify(jsonData)); <-----check data
// may parse your jsonData if required
this.fields = jsonData ;
let keys = Object.keys(jsonData);
console.log(keys);
}
}
parent.component.html
<child-component *ngIf="fields" [childFields] = "fields"></child-component>
Assuming your component is somewhat like below
export class SomeComponent implements OnInit {
public fields: any[];
ngOnInit() {
this.jsonService.fromJSON("users.json").subscribe(fields => {
this.fields = fields;
console.log(this.fields); // Log (I): this.fields is well defined
});
console.log(this.fields); // Log (II): this.fields is undefined
}
}
Then you can either initialize fields with an empty array
public fields: any[] = [];
OR in template
<child-component *ngIf="fields" [childFields]="fields"></child-component>
Is there a way to pass arguments rendered on the backend to angular2 bootstrap method? I want to set http header for all requests using BaseRequestOptions with value provided from the backend. My main.ts file looks like this:
import { bootstrap } from '#angular/platform-browser-dynamic';
import { AppComponent } from "./app.component.ts";
bootstrap(AppComponent);
I found how to pass this arguments to root component (https://stackoverflow.com/a/35553650/3455681), but i need it when I'm fireing bootstrap method... Any ideas?
edit:
webpack.config.js content:
module.exports = {
entry: {
app: "./Scripts/app/main.ts"
},
output: {
filename: "./Scripts/build/[name].js"
},
resolve: {
extensions: ["", ".ts", ".js"]
},
module: {
loaders: [
{
test: /\.ts$/,
loader: 'ts-loader'
}
]
}
};
update2
Plunker example
update AoT
To work with AoT the factory closure needs to be moved out
function loadContext(context: ContextService) {
return () => context.load();
}
#NgModule({
...
providers: [ ..., ContextService, { provide: APP_INITIALIZER, useFactory: loadContext, deps: [ContextService], multi: true } ],
See also https://github.com/angular/angular/issues/11262
update an RC.6 and 2.0.0 final example
function configServiceFactory (config: ConfigService) {
return () => config.load();
}
#NgModule({
declarations: [AppComponent],
imports: [BrowserModule,
routes,
FormsModule,
HttpModule],
providers: [AuthService,
Title,
appRoutingProviders,
ConfigService,
{ provide: APP_INITIALIZER,
useFactory: configServiceFactory
deps: [ConfigService],
multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
If there is no need to wait for the initialization to complete, the constructor of `class AppModule {} can also be used:
class AppModule {
constructor(/*inject required dependencies */) {...}
}
hint (cyclic dependency)
For example injecting the router can cause cyclic dependencies.
To work around, inject the Injector and get the dependency by
this.myDep = injector.get(MyDependency);
instead of injecting MyDependency directly like:
#Injectable()
export class ConfigService {
private router:Router;
constructor(/*private router:Router*/ injector:Injector) {
setTimeout(() => this.router = injector.get(Router));
}
}
update
This should work the same in RC.5 but instead add the provider to providers: [...] of the root module instead of bootstrap(...)
(not tested myself yet).
update
An interesting approach to do it entirely inside Angular is explained here https://github.com/angular/angular/issues/9047#issuecomment-224075188
You can use APP_INITIALIZER which will execute a function when the
app is initialized and delay what it provides if the function returns
a promise. This means the app can be initializing without quite so
much latency and you can also use the existing services and framework
features.
As an example, suppose you have a multi-tenanted solution where the
site info relies on the domain name it's being served from. This can
be [name].letterpress.com or a custom domain which is matched on the
full hostname. We can hide the fact that this is behind a promise by
using APP_INITIALIZER.
In bootstrap:
{provide: APP_INITIALIZER, useFactory: (sites:SitesService) => () => sites.load(), deps:[SitesService, HTTP_PROVIDERS], multi: true}),
sites.service.ts:
#Injectable()
export class SitesService {
public current:Site;
constructor(private http:Http, private config:Config) { }
load():Promise<Site> {
var url:string;
var pos = location.hostname.lastIndexOf(this.config.rootDomain);
var url = (pos === -1)
? this.config.apiEndpoint + '/sites?host=' + location.hostname
: this.config.apiEndpoint + '/sites/' + location.hostname.substr(0, pos);
var promise = this.http.get(url).map(res => res.json()).toPromise();
promise.then(site => this.current = site);
return promise;
}
NOTE: config is just a custom config class. rootDomain would be
'.letterpress.com' for this example and would allow things like
aptaincodeman.letterpress.com.
Any components and other services can now have Site injected into
them and use the .current property which will be a concrete
populated object with no need to wait on any promise within the app.
This approach seemed to cut the startup latency which was otherwise
quite noticeable if you were waiting for the large Angular bundle to
load and then another http request before the bootstrap even began.
original
You can pass it using Angulars dependency injection:
var headers = ... // get the headers from the server
bootstrap(AppComponent, [{provide: 'headers', useValue: headers})]);
class SomeComponentOrService {
constructor(#Inject('headers') private headers) {}
}
or provide prepared BaseRequestOptions directly like
class MyRequestOptions extends BaseRequestOptions {
constructor (private headers) {
super();
}
}
var values = ... // get the headers from the server
var headers = new MyRequestOptions(values);
bootstrap(AppComponent, [{provide: BaseRequestOptions, useValue: headers})]);
In Angular2 final release, the APP_INITIALIZER provider can be used to achieve what you want.
I wrote a Gist with a complete example: https://gist.github.com/fernandohu/122e88c3bcd210bbe41c608c36306db9
The gist example is reading from JSON files but can be easily changed to read from a REST endpoint.
What you need, is basically:
a) Set up APP_INITIALIZER in your existent module file:
import { APP_INITIALIZER } from '#angular/core';
import { BackendRequestClass } from './backend.request';
import { HttpModule } from '#angular/http';
...
#NgModule({
imports: [
...
HttpModule
],
...
providers: [
...
...
BackendRequestClass,
{ provide: APP_INITIALIZER, useFactory: (config: BackendRequestClass) => () => config.load(), deps: [BackendRequestClass], multi: true }
],
...
});
These lines will call the load() method from BackendRequestClass class before your application is started.
Make sure you set "HttpModule" in "imports" section if you want to make http calls to the backend using angular2 built in library.
b) Create a class and name the file "backend.request.ts":
import { Inject, Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class BackendRequestClass {
private result: Object = null;
constructor(private http: Http) {
}
public getResult() {
return this.result;
}
public load() {
return new Promise((resolve, reject) => {
this.http.get('http://address/of/your/backend/endpoint').map( res => res.json() ).catch((error: any):any => {
reject(false);
return Observable.throw(error.json().error || 'Server error');
}).subscribe( (callResult) => {
this.result = callResult;
resolve(true);
});
});
}
}
c) To read the contents of the backend call, you just need to inject the BackendRequestClass into any class of you choice and call getResult(). Example:
import { BackendRequestClass } from './backend.request';
export class AnyClass {
constructor(private backendRequest: BackendRequestClass) {
// note that BackendRequestClass is injected into a private property of AnyClass
}
anyMethod() {
this.backendRequest.getResult(); // This should return the data you want
}
}
Let me know if this solves your problem.
Instead of having your entry point calling bootstrap itself, you could create and export a function that does the work:
export function doBootstrap(data: any) {
platformBrowserDynamic([{provide: Params, useValue: new Params(data)}])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
}
You could also place this function on the global object, depending on your setup (webpack/SystemJS). It also is AOT-compatible.
This has the added benefit to delay the bootstrap, whenit makes sense. For instance, when you retrieve this user data as an AJAX call after the user fills out a form. Just call the exported bootstrap function with this data.
The only way to do that is to provide these values when defining your providers:
bootstrap(AppComponent, [
provide(RequestOptions, { useFactory: () => {
return new CustomRequestOptions(/* parameters here */);
});
]);
Then you can use these parameters in your CustomRequestOptions class:
export class AppRequestOptions extends BaseRequestOptions {
constructor(parameters) {
this.parameters = parameters;
}
}
If you get these parameters from an AJAX request, you need to bootstrap asynchronously this way:
var appProviders = [ HTTP_PROVIDERS ]
var app = platform(BROWSER_PROVIDERS)
.application([BROWSER_APP_PROVIDERS, appProviders]);
var http = app.injector.get(Http);
http.get('http://.../some path').flatMap((parameters) => {
return app.bootstrap(appComponentType, [
provide(RequestOptions, { useFactory: () => {
return new CustomRequestOptions(/* parameters here */);
}})
]);
}).toPromise();
See this question:
angular2 bootstrap with data from ajax call(s)
Edit
Since you have your data in the HTML you could use the following.
You can import a function and call it with parameters.
Here is a sample of the main module that bootstraps your application:
import {bootstrap} from '...';
import {provide} from '...';
import {AppComponent} from '...';
export function main(params) {
bootstrap(AppComponent, [
provide(RequestOptions, { useFactory: () => {
return new CustomRequestOptions(params);
});
]);
}
Then you can import it from your HTML main page like this:
<script>
var params = {"token": "#User.Token", "xxx": "#User.Yyy"};
System.import('app/main').then((module) => {
module.main(params);
});
</script>
See this question: Pass Constant Values to Angular from _layout.cshtml.