I'm currently working on an Angular project and I am creating unit testing for a component using Karma + Jasmine, so I have HTML that has a ngIf calling the API Service as:
HTML
<div class="row" *ngIf="apiService.utilsService.userBelongsTo('operations')"></div">
The service in the *ngIf is the service that I want to mock on the spec.ts below
TS
export class CashFlowSalariesComponent implements OnInit, OnChanges {
constructor(
public apiService: ApiService,
) {}
SPECT.TS
describe('CashFlowSalariesComponent', () => {
let fixture: ComponentFixture < CashFlowSalariesComponent > ;
let mockApiService;
let data;
beforeEach(async(() => {
data = [{
id: 1006,
role: "Developer",
...
}]
mockApiService = jasmine.createSpyObj(['userBelongsTo'])
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
BrowserModule,
HttpClientTestingModule,
ToastrModule.forRoot({
positionClass: 'toast-bottom-right'
})
],
declarations: [
CashFlowSalariesComponent,
],
providers: [{
provide: ApiService,
useValue: mockApiService
}, UserService, ProfileService, VettingStatusService, ApplicationRoleService,
SeniorityLevelService, PlacementStatusService, EducationLevelService, UtilsService, ShirtSizeService,
CountryService, CityService, PostalCodeService, StateService, ClientSectorService, JobService, ProfileActivityService, ProfileSalaryActivityService, ClientService, RequestTimeOffService, TimeOffTypeService, PulsecheckDetailService, PulsecheckMasterService,
PulsecheckQuestionService, ExpenseService, DepartmentService, ExchangeRateService, SkillCategoriesService, ProfileRoleService,
ToastrService
]
})
fixture = TestBed.createComponent(CashFlowSalariesComponent);
}));
it('should set salaries data correctly', () => {
mockApiService.userBelongsTo.and.returnValue(of('operations'))
debugger;
fixture.detectChanges();
})
As you see, I tried to create mock of api service as: mockApiService = jasmine.createSpyObj(['userBelongsTo']), then use in the it as: mockApiService.userBelongsTo.and.returnValue(of('operations')), but when I debug it throws as unknown as the following picture
and the test return the following error:
Cannot read properties of undefined (reading 'userBelongsTo')
I do not know if this happen because userBelongs to it is inside another service inside apiService:
ApiService
#Injectable()
export class ApiService {
public utilsService: UtilsService;
constructor(private injector: Injector) {
this.utilsService = injector.get(UtilsService);
}
}
Utils.Service:
userBelongsTo(groupName: string) {
return this.groups.split(',').reduce((c, g) => c || g.toUpperCase() == groupName.toUpperCase(), false);
}
How can I make this work? Regards
Dependency injections should be private, why does the template HTML needs to handle the service call? Instead of delegating the service call to the template, make a proper function in the component, as a result, your template will be cleaner
<div class="row" *ngIf="getBelongsTo()"></div">
constructor(private apiService: ApiService) {}
getBelongsTo(): boolean {
// I guess userBelongsTo returns either a string or undefined,
// so using !! creates a valid boolean
// if userBelongsTo returns a valid string, returns TRUE
// if returns undefined/null returns FALSE
return !!this.apiService.utilsService.userBelongsTo('operations');
}
For testing, you need to provide the mock/fake value from userBelongsTo before starting to test. Additionally, the way you are mocking the service is wrong, it could be like the following:
const mockApiService = jasmine.createSpyObj<ApiService>('ApiService', ['userBelongsTo']);
let data;
beforeEach(() => {
data = [{ id: 1006, role: "Developer", ...}];
mockApiService.userBelongsTo.and.returnValue(of('operations'));
}
beforeEach(async() => {
TestBed.configureTestingModule({
declarations: [...],
provides: [{provide: ApiService, useValue: mockApiService}]
})
})
I do not know if this happen because userBelongs to it is inside another service inside apiService
When unit testing, you don't care how any dependecy is implemented when is injected in the component, you are mocking all dependencies so it doesn't matter.
One thing to notice, since you provided how ApiService is implemented, keep in mind that somewhere, in some module, ApiService needs to be added in the providers array since it isn't provided as root, e.x: #Injectable({ provideIn: 'root' })
Related
I have the following component:
#Component({
selector: 'my-form',
templateUrl: './my-form.component.html',
})
export class MyFormComponent implements OnInit {
#Input('company') company: CompanyInfo;
private config: ConfigInterface | null;
constructor(private companyService: CompanyService, private something: Something, private dialog: MatDialog) {
}
ngOnInit() {
....
}
theMtehodWhichIWantToTest(company: string) {
if (someTest.indexOf(domain)) {
this.dialog.open(MyAllertComponent, {
data: {
title: 'blah',
},
});
}
}
}
My poor and failing unit test:
describe( 'MyFormComp', () => {
it('should open MatDialog if email is from popular domain', () => {
const dialog = {} as MatDialog;
const comp = new MyComponent({} as CompanyService, {} as Something, dialog);
comp.getCompanys(company);
expect(dialog.open(AlertComponent));
});
})
The error message: TypeError: dialog.open is not a function
Yes, I know why I have this error message - I am not mocking the dialog correctly and the functon open is not available. Can someone tell me how to make open available using jasmine (I.e. how to properly mock MatDialog?)
I am quite new to js unit testing so, just telling me what to google will not be very helpful for me.
You can use the beforeEach method to create spy's for your component dependencies:
emailServiceSpy = jasmine.createSpyObj('EmailService', {});
somethingSpy = jasmine.createSpyObj('Something', {});
dialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
Then provide these for your Testmoudle:
TestBed.configureTestingModule({
declarations: [YourComponent],
providers: [
{ provide: EmailService, useValue: emailServiceSpy },
{ provide: Something, useValue: somethingSpy },
{ provide: MatDialog, useValue: dialogSpy },
],
imports: [],
}).compileComponents();
And in your Test you can check if your spy-function get called:
expect(dialogSpy.open).toHaveBeenCalled();
I want to know how to mock ActivatedRoute url.
I am getting current url by using ActivatedRoute
this.activatedRoute.url.subscribe(url => {
this.isEdit = false;
if (url[0].path === 'updatecoc') {
this.isEdit = true;
}
});
So I want to mock url in ActivatedRoute
I have tried this
let fakeActivatedRoute = new MockActivatedRoute();// MockActivatedRoute I have implemented this class from MockActivatedRoute class
fakeActivatedRoute.parent = new MockActivatedRoute();
let urlSegment: UrlSegment[] = [];
urlSegment.push({ path: "updatecoc", parameters: {} });
fakeActivatedRoute.url = Observable.of(urlSegment);
TestBed.configureTestingModule({ { provide: ActivatedRoute, useValue: fakeActivatedRoute }})
But I am getting error:
unable to get property 'subscribe' of undefined or null reference
I don't know where I am missed. How can I resolve this issue?
I have a better solution for you :
import { RouterTestingModule } from '#angular/router/testing';
TestBed.configureTestingModule({
imports: [RouterTestingModule],
// ...
})
.compileComponents();
This will mock your whole routing module, now you can inject your dummy mock into your providers and spy on the functions like so
providers: [{provide: ActivatedRoute, useValue: {}}]
And when you test a function calling, let's say, myMock (I know it's not in it, it's for the example) :
const mock = TestBed.get(ActivatedRoute);
spyOn(mock, 'myMock').and.returnValue(/* what you want */);
// ...
expect(mock.myMock).toHaveBeenCalledWith(/* params here */);
EDIT I quickly looked at what url is made of, here is your mock :
mock.url = jasmine
.createSpy()
.and
.returnValue(new BehaviorSubject({
path: 'yout-mocked-path',
parameters: {/* your mocked parameters */}
})));
I'm trying to get some mocked results for my development environment. I've tried to incorporate angular-in-memory-web-api without much success. Here's my code:
app.module.ts:
#NgModule({
declarations: [
AppComponent,
],
imports: [
...
HttpModule,
...
InMemoryWebApiModule.forRoot(MockEventData, {
passThruUnknownUrl: true
})
],
providers: [
...
{
provide: Http,
useClass: ExtendedHttpService
},
...
{
provide: EventService,
useFactory: (http: Http, userService: UserService, newEventService: NewEventService, router: Router) => {
if (environment.production) {
return new EventService(http, userService, newEventService, router)
} else {
return new MockEventService(http, userService, newEventService, router)
}
},
deps: [Http, UserService, NewEventService, Router]
}
],
bootstrap: [AppComponent]
})
export class AppModule {
mock-event.service.ts:
#Injectable()
export class MockEventService {
private imageUploadBatch: Observable<Boolean>[];
private fakeResponse;
constructor(
private http: Http,
private userService: UserService,
private newEventService: NewEventService,
private router: Router,
) {
};
getEvents(excludedEvents: string[]): Observable<Event[]> {
return this.http
.post('api/events', excludedEvents)
.map((res: Response) => res.json())
.publishLast().refCount()
.catch((error: any) => Observable.throw(error.json().error || 'Show error.'));
}
}
mock-event-data.ts:
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class MockEventData implements InMemoryDbService {
createDb() {
let events = [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
];
return { events };
}
}
The code is quite simple. I made it following this guide: https://angular.io/docs/ts/latest/guide/server-communication.html. However, for whatever reason, the POST for /events always returns {data: Array[0]}.
Any help provided will be deeply appreciated.
Thanks!
post method will NOT just retrieve the data to angular-in-memory-web-api. Instead it will create the entity accordingly. It's essential and default behavior is to send data to the server same as put and delete. Of course there will be a response to the post request which is probably the data in angular-in-memory-web-api case but remember, this totally depends on the server to response. On the otherget should be used for the purpose of retrieving data from angular-in-memory-web-api as you want.
Well, as it turns out, POST seems to not return data in angular-in-memory-web-api. I succeeded in retrieving data by using a GET request. This isn't ideal, but it'll have to work for now.
If someone has a better answer, please provide it, as it's a bit iffy to commit a mock that doesn't use the original request types.
Thanks!
Below error shown when I ran ng test command.
Here is my service spec,
describe('BackendService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: Http, useFactory: (backend, options) => {
return new Http(backend, options);
},
deps: [MockBackend, BaseRequestOptions]
},
MockBackend,
BaseRequestOptions,
BackendService
]
});
});
it('should ...', inject([BackendService, MockBackend], (service: BackendService) => {
expect(service).toBeTruthy();
})
);
});
BackendService.ts looks like,
export class BackendService {
private baseUrl: string = 'https://foo-backend.appspot.com/_ah/api/default/v1';
constructor(private http: Http, baseName: string) {
this.baseUrl = this.baseUrl + baseName;
}
.....
}
It seems like extra parameter inside the BackendService class's constructor causes this problem..
How do you expect Angular to know what baseName is supposed to be? All constructor parameters need to be obtained from the Injector. And if there is no corresponding token for the parameter, then it can't be looked up.
You can add a token by doing
// somewhere in some file
import { OpaqueToken } from '#angular/core';
export const BASE_NAME_TOKEN = new OpaqueToken("app.base_name");
// in test class
import { BASE_NAME_TOKEN } from 'where-ever'
TestBed.configureTestingModule({
providers: [
BackendService,
{ provide: BASE_NAME_TOKEN, useValue: 'whatever-the-base-is' }
]
});
// in service constructor
import { Inject } from '#angular/core'
import { BASE_NAME_TOKEN } from 'where-ever'
constructor(http: Http, #Inject(BASE_NAME_TOKEN) baseName: string) {}
See Also:
Dependency Injection Tokens
What is the difference between #Inject vs constructor injection as normal parameter in Angular 2?
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.