angular: How to mock extends class in unit test - javascript

I've a injectable service (EntityApi) which extends a class (BaseApi). In my spec I like to mock the BaseApi with BaseApiStub. But it vain. Always calling the EntityApi.
// class
export class BaseApi { // want to mock BaseApi
constructor(injector: Injector) {
console.log("Should not be here...");
}
}
// service
#Injectable()
export class EntityApi extends BaseApi {
constructor(injector: Injector) {
super(injector, "entity");
}
}
// component
#Component({
selector: 'rt-entity-list',
templateUrl: './entity-list.component.html',
})
export class EntityListComponent {
api: any;
constructor(public entityApi: EntityApi) {
this.api = entityApi;
}
}
// mock api
export class BaseApiStub { //mocked api
constructor() {
console.log("You are on track!!")
}
get() { }
}
// spec
describe('EntityListComponent', () => {
let component: EntityListComponent;
let fixture: ComponentFixture<EntityListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [EntityListComponent],
providers: [ { provide: BaseApi, useClass: BaseApiStub }, // mocked class.
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
beforeEach(() => {
fixture = TestBed.createComponent(EntityListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Expected Behavior is, while compile component in spec. It should call the BaseApiStub, instead it is calling BaseApi. I've seen a solution as below. But no luck.
export class BaseApiStub extends BaseApi { }
Test Code: stackblitz Check the console. I expect the You are
on track!! log but received as Should not be here...
Not able to progress further. Can someone correct my mistake please.

What you are trying to do does not work. Dependency injection and class inheritance are not directly related. This means you cannot switch out the base class of your service like this.
As I see it you have two ways on how to do this.
Option 1:
Instead of mocking your BaseApi and providing the mock in your test you need to mock your EntityApi and provide this mock in your test.
Option 2:
Instead of letting your EntityApi extend from BaseApi, you could keep BaseApi a simple service and provide it as a dependency.
Instead of
class EntityApi extends BaseApi {
constructor(private injector: Injector) {
you do
class EntityApi {
constructor(private api: BaseApi) {
If you setup your EntityApi like this, it does not extend from BaseApi, but rather has it as a dependency. Then you can create a mock of the BaseApi and provide it like you did in your test.
Edit
Regarding your comment:
Since I should be using methods from BaseApi I cannot go without extends.
This is not true. Let's say the BaseApi has a method foo() that you want to use. When you extend your baseclass, the usage might look like this:
class EntityApi extends BaseApi {
constructor(private injector: Injector) {}
exampleMethod() {
this.foo();
}
}
If you just have the dependency you can still call the method like this:
class EntityApi {
constructor(private api: BaseApi) {}
exampleMethod() {
this.api.foo();
}
}
You don't need to extend from BaseApi in order to call methods on it.

In case you need to mock a method of a parent class (e.g for Directive) you can do that through the stub extension of the tested class.
spyObject = {
methodToSpyOn(){}
};
#Directive({selector: '[myDirective]'})
class MyStubDirective extends MyDirective {
parentMethodToMock() {
return spyObject.methodToSpyOn();
}
}
spyOn(spyObject, 'methodToSpyOn').and.returnValue(true);
This approach is usually needed if your class has the parent method calls in constructor

Related

Angular. How to switch component depending on service's actions

Lets say I have 2 components, aComponent and bComponent. I have them redered inside the AppComponent
<app-a>
<app-b>
And I have service myService that has method .trigger().
What I want is to show only aComponent, but whenever I call myService.trigger() from another part of code, it would switch and show bComponent. That's perfect implementation that I can't reach.
Question is: Is it possible to do so? And if not what is the best closest solution.
The only working solution I got:
I added .trigger() inside AppComponent
export class AppComponent {
title = 'spa';
show: boolean = false;
trigger() {
this.show = true;
}
}
And rendered components like so:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
Then whenever I want to trigger switching, I add instance of the app to the constructor and call it's method:
export class AnotherComponent implements OnInit {
constructor(
private app: AppComponent
) {}
ngOnInit(): void {
this.app.trigger();
}
}
Even though it's working pretty good, I myself see that it's a dirty solution. Components are not intended to be used inside another components, but Services are.
You can use Subject from rxjs library for that.
In your service file:
// a-service.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable({ providedIn: 'root' })
export class AService {
private subject = new Subject<any>();
trigger(state: boolean) {
this.subject.next(state);
}
getTrigger(): Subject<any> {
return this.subject;
}
}
and in your AppComponent:
// app.component.ts
...
private show = false;
constructor (private aService: AService) { }
ngOnInit() {
this.aService.getTrigger().subscribe(state => {
this.show = state;
});
}
the template can be as you provided - it's fine:
<div *ngIf="!show; else show">
<app-a></app-a>
</div>
<ng-template #show>
<app-b></app-b>
</ng-template>
And if you want to trigger from another component, you do it like this:
// another.component.ts
...
constructor (private aService: AService) { }
ngOnInit() {
this.aService.trigger(true);
}
One way to communicate between different components and services which aren't directly related, is via 'Subjects'.
You can try to create a subject and pass in values to it from myService.trigger(). And you can subscribe to that subject from whichever component you want to access that trigger data.

Using #ViewChild { read: ElementRef } of component causes unit test to fail

In my component I have a child component that looks like this:
<child-component #childComponent></childComponent>
In my parent component I then access this child component using #ViewChild and the read parameter to get the ElementRef, and not the component reference. I need the ElementRef to ensure I can get some properties from nativeElement that I need. So it's like this:
export class ParentComponent {
#ViewChild('childComponent', { read: ElementRef }) public childComponent: ElementRef;
public position: string;
// some way down the code
private someMethod() {
if (this.childComponent.nativeElement.offsetLeft > 500) {
this.position = 'left';
} else {
this.position = 'right';
}
}
}
So this works for the application, however I am writing the tests and mocking the child component, like this:
#Component({
selector: 'child-component',
template: ''
})
class ChildComponentMockComponent {
private nativeElement = {
get offsetLeft() {
return 600
}
};
}
beforeEach(async(() => TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ParentComponent, ChildComponentMockComponent ],
providers: [ ... ],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents()));
it('should have the correct position, based on position of child-component', () => {
spyOn(component, 'someMethod');
expect(component.someMethod).toHaveBeenCalled();
expect(component.position).toBe('left');
});
So the test will compile the component, and use the mocked child component values as the proper value and compute the value of this.position, which is then asserted in the test.
However, when the { read: ElementRef } parameter is set, the mock gets completely ignored by the TestBed, even though it's being added in the declarations array. If I remove { read: ElementRef }, the mock is used in the test and it passes. But then my application doesn't work, as it is now getting the component reference, where the nativeElement property doesn't exist, rather than the element reference.
So how do I get the ElementRef in my application and then in my test use the mock component?
I have fixed this by changing the architecture of the app. The child component now finds it's own offsetLeft property, and then puts it into an output EventEmitter to be picked up the parent component.
export class ChildComponent implements AfterViewInit {
#Output() offsetPosition: EventEmitter<number> = new EventEmitter<number>();
constructor(private el: ElementRef) {}
public ngAfterViewInit() {
this.offsetPosition.emit(this.el.nativeElement.offsetLeft);
}
}
export class ParentComponent implements AfterViewInit {
public childComponentOffset: number;
public ngAfterViewInit() {
setTimeout(() => {
// this.childComponentOffset is available
// needs to be in setTimeout to prevent ExpressionChangedAfterItHasBeenCheckedError
// more info: https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4
}
}
public getChildComponentOffset(position: number): void {
this.childComponentOffset = position;
}
}
And then in the HTML, you just define the child component with output variable and method:
<child-component (offsetPosition)="getChildComponentOffset($event)"></child-component>
In the test, I then mock ElementRef for the child component and use it as a provider.
const mockElementRef: any = {
get offsetLeft() {
return position;
}
};
beforeEach(async(() => TestBed.configureTestingModule({
imports: [ ... ],
declarations: [ ParentComponent ],
providers: [
{ provide: ElementRef, useValue: mockElementRef }
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents()));
it('should have the correct position, based on position of child-component', (done) => {
component.getChildComponentOffset(600);
setTimeout(() => expect(component.position).toBe('left'));
done();
});

Multiple instance of angular 4 directive called from a component mesed up the input values

I have a component in angular 4 that is called three times. In template metadata I have a div with a directive with some bindings like this.
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {};
this.cOptions = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
//this.report.opt is binded to a component when is instantiated.
//this.gServ.objectMerge is a function that merge the two objects
}
}
this.cOptions change for every instance of the component, then in the directive I have this:
import { Directive, ElementRef, HostListener, Input, OnInit } from '#angular/core';
#Directive({
selector: '[gDirective]'
})
export class SGDirective implements OnInit {
public _element: any;
#Input() public cOptions: string;
constructor(public element: ElementRef) {
this._element = this.element.nativeElement;
}
ngOnInit() {
console.log(this.cOptions);
}
}
The problem is that console.log(this.cOptions); always print the same object, even when component set cOptions with diferent values in ngOnInit method of the compnent.
Do you have some idea what is wrong?
Your component property binding [cOptions]="dataChart" doesn't look good, reason being your dataChart is not even defined. it should be like [DIRECTIVE_PROPERTY]="COMPONENT_PROPERTY" and your COMPONENT_PROPERTY is not even defined in SGComponent component class.
Your component class should be something like this:
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="Options">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = {};
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
#Ashwani points out a valid problem with your code. The way your template is wiring things up, nothing will ever be passed to the SGDirective input.
Another potential problem you could be running into has to do with the gServ code. If gServ is a singleton (which is probably the case) and it is returning the same object to each of the SGComponents, then all the SGDirectives will have the same value. A simple way to test this is to put {{Options | json}} in the SGComponent template.
To create a new instance of the gServ service for each SGComponent you can add a providers array to the #Component metadata. It would look like this:
import {gServ} from '../gServ.service';
#Component({
selector: 'sr-comp',
template: `{{Options | json}}<div gDirective [cOptions]="Options"></div>`
providers: [gServ],
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
You have probably the same return/value at this.gServ.objectMerge) (you can test it wihtout calling the service, and passing each one one different objet make by you)
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
//#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {nicolas: 'nicolas1'}; //change this in the next component that use the directive
}
}
If that is the case, your problem is that gServ is provide at the same rootComponent. with angular, service provider at the same rootComponent are singleton.
And use the same type in your directive and your component!!

Injecting service for another service

I have few levels of services calling each other, i.e. a ComponentService that uses a DataService that uses an ErrorService.
Now, one of the components services, needs to specify that the ErrorService instance used by the DataService should be of e.g. SpecialErrorService type. - and it does not use the ErrorService or the SpecialErrorService directly.
How can I instruct the data service to inject the correct ErrorService type when instantiating? (Note that I want this behavour only for a specific ComponentService, not as a general case)
#Injectable()
export class SpecialComponentService {
constructor(private _dataService: DataService){}
log() {
//expecting this to log: I'm a special error service
this._dataService.log();
}
}
#Injectable()
export class RegularComponentService {
constructor(private _dataService: DataService){}
log() {
//expecting this to log: I'm a regularerror service
this._dataService.log();
}
}
#Injectable()
export class DataService {
constructor(private _errorService: ErrorService){}
log() {
this._errorService.log();
}
}
#Injectable()
export class ErrorService {
log(){
console.log("I'm a regular error service");
}
}
#Injectable()
export class SpecialErrorService extends ErrorService {
log(){
console.log("I'm a special error service");
}
}
When providing the service you can substitute it by a special service.
For example if you use it within a component, you can provide it like this:
{ provide: ErrorService, useClass: SpecialErrorService }
PS: I wanted to post this as comment but I'm not allowed to. Says I need a 50 reputation...

Extending factories in AngularJS TypeScript

I want to extend a angular factory with typescript, so I can have a specific factory, but it gives me an error when I try to run it.
module App.Services {
export interface IShared {
documents: Array<any>;
}
export class Shared implements IShared {
public static serviceId = "shared";
datacontext: App.Services.IDatacontext;
documents: Array<any>;
constructor(datacontext: App.Services.IDatacontext) {
this.datacontext = datacontext;
this.datacontext.getDocuments().success((documents) => {
this.documents = documents;
});
}
}
editorApp.factory(Shared.serviceId, ['datacontext',
(datacontext) => new Shared(datacontext)
]);
}
I want to extend the Shared class to the following class
module App.Services {
export interface ITemplateShared extends IShared {
}
export class TemplateShared extends Shared implements ITemplateShared {
public static serviceId = "templateShared";
datacontext: App.Services.IDatacontext;
constructor(datacontext) {
super(datacontext);
//this.datacontext = datacontext;
}
}
editorApp.factory(TemplateShared.serviceId, ['datacontext', 'shared',
(datacontext) => new TemplateShared(datacontext)
]);
}
When I start it gives me an Unhandled exception at line 6, column 5 in templateShared.js
Thanks in advance!
class TemplateShared extends Shared
Most likey issue: make sure Shared is loaded by the browser before TemplateShared. The order of loading in JavaScript / TypeScript is important.
I recommend using external modules to avoid this mistake : https://www.youtube.com/watch?v=KDrWLMUY0R0&hd=1

Categories