So I am doing some unit tests. And the following component I try to unit test:
export class EcheqDisplayComponent implements OnInit {
echeq: EcheqSubmissionApi;
orgId = 1;
constructor(
private route: ActivatedRoute
) {
this.echeq = this.route.snapshot.data['submission'];
}
ngOnInit() {
}
getAnswers(page: EcheqPageApi): any[] {
return page.elements.map(element => this.echeq.answers[element.name]);
}
And this is the unit test for it:
describe('EcheqDisplayComponent', () => {
let component: EcheqDisplayComponent;
let fixture: ComponentFixture<EcheqDisplayComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ],
imports:[ParticipantEcheqModule,
RouterModule.forRoot([])
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EcheqDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should create component', () => {
expect(component).toBeTruthy();
});
});
So I dont get errors.
But my question is. Is this the correct way to do ti?
Because is it not better to use a mock for ActivatedRoute?
An if yes, how to do it then the correct way?
Thank you
sO THIS IS the template of it:
<div class="echeq-display" *ngIf="echeq">
<header class="echeq-display-header header">
<div class="echeq-display-info">
<h1 class="heading echeq-display-heading">
{{ echeq.definition.title }}
</h1>
<div class="sub-heading echeq-display-subheading">
<span class="echeq-display-creator">
Toegekend door:
{{ echeq.assignedByProfName ? echeq.assignedByProfName : 'Het Systeem' }}
</span>
<span class="echeq-display-date">{{
echeq.definition.createdOnUtc | date: 'dd MMM'
}}</span>
</div>
</div>
<app-meta-box
[metadata]="{
numPages: echeq.definition.numPages,
vPoints: echeq.definition.awardedVPoints
}"
></app-meta-box>
</header>
<main class="echeq-display-questions body">
<app-echeq-question
*ngFor="let page of echeq.definition.pages; let i = index"
[page]="page"
[readonly]="true"
[number]="i + 1"
[answers]="getAnswers(page)"
></app-echeq-question>
</main>
</div>
and the unit test like this:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ],
providers: [
{ provide: ActivatedRoute, useClass: MockActivatedRoute }
],
imports:[
ParticipantEcheqModule
]
})
.compileComponents();
}));
and this is the app-meta-box component:
export class MetaData {
numPages: number;
vPoints: number;
}
#Component({
selector: 'app-meta-box',
templateUrl: './meta-box.component.html',
styleUrls: ['./meta-box.component.scss']
})
export class MetaBoxComponent implements OnInit {
#Input() metadata: MetaData;
constructor() {}
ngOnInit() {}
}
You can simply provide mock ActivatedRoute object with necessary variable set in "providers" property when "TestBed.configureTestingModule({...})" is called to configure testing module.
class MockActivatedRoute {
public snapshot = {
data: {
submission: {
answers: {}
}
}
};
}
TestBed.configureTestingModule({
declarations: [ ],
providers: [
{ provide: ActivatedRoute, useClass: MockActivatedRoute }
]
imports:[
ParticipantEcheqModule,
// RouterModule.forRoot([]) // Not required after mocking the "ActivatedRoute"
]
})
.compileComponents();
This way you can test your component for different values in activatedRoute.
Related
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">
TS
export class CashFlowSalariesComponent implements OnInit, OnChanges {
constructor(
public apiService: ApiService,
) {}
SPECT.TS
describe('CashFlowSalariesComponent', () => {
let fixture: ComponentFixture < CashFlowSalariesComponent > ;
let mockCashFlowService;
let data;
beforeEach(async(() => {
data = [{
id: 1006,
role: "Developer",
...
}]
mockCashFlowService = jasmine.createSpyObj(['createTableData'])
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
BrowserModule,
HttpClientTestingModule,
ToastrModule.forRoot({
positionClass: 'toast-bottom-right'
})
],
declarations: [
CashFlowSalariesComponent,
],
providers: [{
provide: ApiService,
useValue: mockCashFlowService
}, 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 create', () => {
expect(CashFlowSalariesComponent).toBeTruthy();
});
it('should set salaries data correctly', () => {
mockCashFlowService.userBelongsTo = 'operations'
mockCashFlowService.createTableData.and.returnValue( of (data))
debugger;
fixture.detectChanges();
expect(fixture.componentInstance.dataHeaders.length).toBe(10);
})
As you see, I tried to set userBelongsTo as: mockCashFlowService.userBelongsTo = 'operations' but I get an error:
TypeError: Cannot set properties of undefined (setting
'userBelongsTo')
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);
}
It is undefined because you have not defined it. You need to add it to your mock and then have it return something.
mockCashFlowService = jasmine.createSpyObj(['createTableData', 'userBelongsTo']);
https://volaresoftware.com/en/technical-posts/mocking-calls-with-jasmine
I have a karma unit test and the test fails with below error.
this.gridApi.getScaleWidth().subscribe is not a function
GridApi.ts
export class GridApi {
private scaleWidthSubject = new BehaviorSubject<{value: number}>({value: 0});
public getScaleWidth(): Observable<{value:number}> {
return this.scaleWidthSubject;
}
}
GridComponent.ts
export class GridComponent implements OnInit, OnDestroy, AfterViewInit {
private subscribeToValueChanges() {
this.scaleWidth$ = this.gridApi.getScaleWidth().subscribe( width => {
this.scaleWidth = width.value;
});
}
}
Component.spec.ts
describe('GridComponent', () => {
beforeEach(async () => {
const mockGridApiService = jasmine.createSpyObj("GridApi", {
getScaleWidth () : Observable<{value: number}> {
let scaleWidthSubject = new BehaviorSubject<{value: number}>({value: 0});
return scaleWidthSubject.asObservable();
}
});
}
await TestBed.configureTestingModule({
providers: [ { provide: GridApi, useValue: mockGridApiService} ],
imports: [
HttpClientModule
],
declarations: [ GridComponent ]
})
}
What should the mock getScaleWidth() return to pass the test. Not sure what I'm missing here.
describe('GridComponent', () => {
const mockGridService = jasmine.createSpyObj<GridApi>('GridApi', ['getScaleWidth'])
beforeEach(() => {
mockGridService.getScaleWidth.and.returnValue(of({ value: 0 }));
});
await TestBed.configureTestingModule({
providers: [ { provide: GridApi, useValue: mockGridService} ],
imports: [HttpClientModule],
declarations: [ GridComponent ]
})
it('should call getScaleWidth from service', () => {
// the component function that triggers the service call is private
// make the call from component
expect(mockGridService.getScaleWidth).toHaveBeenCalled();
mockGridService.getScaleWidth().subscribe(response => {
expect(response.value === 0)
})
})
}
I want to write test case for a method addItem may be in product.component.spec.ts file . This is my code
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
constructor() { }
productName:any
quantity:any
public data:any[]=[
{
"ID": 101,
"Product": "Moto G",
"Quantity": 2,
},
{
"ID": 102,
"Product": "TV",
"Quantity": 4,
},
{
"ID": 105,
"Product": "Watch",
"Quantity": 2,
},
]
ngOnInit(): void {
}
addItem() {
this.data.push({ Product: this.productName, Quantity: this.quantity});
}
}
I started writing in product.component.spec.ts file
describe('ProductComponent', () => {
let component: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;
fixture = TestBed.createComponent(ProductComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
});
product.component.spec.ts file
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { AppComponent } from '../app.component';
import { ProductComponent } from './product.component';
describe('ProductComponent', () => {
let component: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;
TestBed.configureTestingModule({
declarations: [
AppComponent,
ProductComponent
]
});
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ProductComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProductComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
here is how you should do it.
describe('ProductComponent', () => {
let component: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;
TestBed.configureTestingModule({
declarations: [
AppComponent,
ProductComponent
]
});
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ProductComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProductComponent);
component = fixture.componentInstance;
component.data = [{productName : "p1", quantity : 3}]
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should add items to "data"', () => {
expect(component.data.length).toBe(1); // since you have initialized the variable
component.productName = "Prod1";
component.quantity = 1;
component.addItem(); // this will trigger the method
expect(component.data.length).toBe(4); // this will show that the entry was added in "this.data"
});
});
I would also suggest you to go through this intro article of mine to understand unit testing in angular. There are few links attached to this article as well , which can help you understand the best practices of testing a component. Cheers :)
A testcase always includes 4-5 steps:
Tear up
Data preperation
Execution
Verification
(optional) Tear down
What you already have is the first step. For the second step you need to prepare the required fields, which are data, quantity and productName.
component.productName = 'Awesome Product';
component.quantity = 3;
component.data = [];
The third steps only executes the method you want to test.
component.addItem();
With the fourth step you check the result, which is now the extended array.
expect(component.data).toEqual([{ Product: 'Awesome Product', Quantity: 3 }]);
So i try to do some unit testing on a component. But I have some problems with Input() parameter. And then especially a component in the component
SO I have this component:
export class EcheqDisplayComponent implements OnInit {
echeq: EcheqSubmissionApi;
constructor(
private route: ActivatedRoute
) {
this.echeq = this.route.snapshot.data['submission'];
}
ngOnInit() {
}
getAnswers(page: EcheqPageApi): any[] {
return page.elements.map(element => this.echeq.answers[element.name]);
}
}
and the template:
<div class="echeq-display" *ngIf="echeq">
<header class="echeq-display-header header">
<div class="echeq-display-info">
<h1 class="heading echeq-display-heading">
{{ echeq.definition.title }}
</h1>
<div class="sub-heading echeq-display-subheading">
<span class="echeq-display-creator">
Toegekend door:
{{ echeq.assignedByProfName ? echeq.assignedByProfName : 'Het Systeem' }}
</span>
<span class="echeq-display-date">{{
echeq.definition.createdOnUtc | date: 'dd MMM'
}}</span>
</div>
</div>
<app-meta-box
[metadata]="{
numPages: echeq.definition.numPages,
vPoints: echeq.definition.awardedVPoints
}"
></app-meta-box>
</header>
<main class="echeq-display-questions body">
<app-echeq-question
*ngFor="let page of echeq.definition.pages; let i = index"
[page]="page"
[readonly]="true"
[number]="i + 1"
[answers]="getAnswers(page)"
></app-echeq-question>
</main>
</div>
and the unit test:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { EcheqDisplayComponent } from './echeq-display.component';
import { ParticipantEcheqModule } from '../../participant-echeq.module';
import { RouterModule, ActivatedRoute } from '#angular/router';
import { MockActivatedRoute } from 'src/app/shared/mocks/MockActivatedRoute';
import { MetaData } from '../meta-box/meta-box.component';
describe('EcheqDisplayComponent', () => {
let component: EcheqDisplayComponent;
let fixture: ComponentFixture<EcheqDisplayComponent>;
const metaData: MetaData = new MetaData();
// const metaDataInfo = fixture.debugElement.componentInstance;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ],
providers: [
{ provide: ActivatedRoute, useClass: MockActivatedRoute }
],
imports:[
ParticipantEcheqModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EcheqDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should create component', () => {
metaData.numPages = 20;
expect(component).toBeTruthy();
});
});
But it stay saying:
TypeError: Cannot read property 'numPages' of undefined.
So what I have to change that it is working?
Thank you
So the MetaData is from the app-meta-box component.it looks like this:
export class MetaData {
numPages: number;
vPoints: number;
}
#Component({
selector: 'app-meta-box',
templateUrl: './meta-box.component.html',
styleUrls: ['./meta-box.component.scss']
})
export class MetaBoxComponent implements OnInit {
#Input() metadata: MetaData;
constructor() {}
ngOnInit() {}
}
This is the mock class:
export class MockActivatedRoute {
public snapshot = {
data: {
submission: {
answers: {}
}
}
};
}
I have it now like this:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { EcheqDisplayComponent } from './echeq-display.component';
import { ParticipantEcheqModule } from '../../participant-echeq.module';
import { RouterModule, ActivatedRoute } from '#angular/router';
import { MockActivatedRoute } from 'src/app/shared/mocks/MockActivatedRoute';
import { MetaData } from '../meta-box/meta-box.component';
describe('EcheqDisplayComponent', () => {
let component: EcheqDisplayComponent;
let fixture: ComponentFixture<EcheqDisplayComponent>;
const metaData: MetaData = new MetaData();
// const metaDataInfo = fixture.debugElement.componentInstance;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ],
providers: [
{ provide: ActivatedRoute, useValue: new MockActivatedRoute().withData({submission:{ answers:{} } }) }
],
imports:[
ParticipantEcheqModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EcheqDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should create component', () => {
component.echeq = {
definition: {
title: 'test title',
awardedVPoints: 0,
numPages: 9
}
}
expect(component).toBeTruthy();
});
});
But then I get this error:
Property 'answers' is missing in type '{ definition: { title: string; awardedVPoints: number; numPages: number; }; }' but required in type 'EcheqSubmissionApi'.ts(2741)
echeqSubmissionApi.ts(59, 5): 'answers' is declared here.
I have it now like this:
fit('should create component', () => {
component.echeq = {
definition: {
title: 'test title',
awardedVPoints: 0,
numPages:9
}
} as EcheqSubmissionApi;
expect(component).toBeTruthy();
});
export interface EcheqSubmissionApi {
/**
* Primary key of this submission (server set).
*/
id?: string;
definition?: EcheqDefinitionApi;
/**
* Id of the prof who assigned this eCheq (server set).
*/
assignedByProfId?: string;
/**
* Name of the prof who assigned this eCheq (server set). Only set on get operations. May be null if assigned by the system.
*/
assignedByProfName?: string;
/**
* Id of the organisation who assigned this eCheq (server set).
*/
assignedByOrgId?: number;
/**
* Participant ID of the patient this eCheq is assigned to (server set).
*/
assignedToId?: string;
/**
* When this submission was assigned (UTC, server set).
*/
assignedOnUtc?: Date;
/**
* If set until when the eCheq can be submitted.
*/
validUntilUtc?: Date;
/**
* Whether the eCheq has been started and whether it has been submitted (server set).
*/
status?: EcheqSubmissionApi.StatusEnum;
/**
* When this submission was completed (UTC, server set).
*/
submittedOnUtc?: Date;
/**
* Answers of form. In the form of a json object with name and value of the questions { \"nameOfQuestion\" : \"valueOfQuestion\" }
*/
answers: object;
/**
* initialValues of form. In the form of a json object with name and value of variables { \"nameOfQuestion\" : \"valueOfQuestion\" }
*/
initialValues?: object;
/**
* The page the participant is currently on
*/
currentPage?: number;
/**
* The progress of the echeq in percentage
*/
progress?: number;
}
export namespace EcheqSubmissionApi {
export type StatusEnum = 'New' | 'Active' | 'Submitted';
export const StatusEnum = {
New: 'New' as StatusEnum,
Active: 'Active' as StatusEnum,
Submitted: 'Submitted' as StatusEnum
};
}
But if I run the unit test, I still get this error:
TypeError: Cannot read property 'numPages' of undefined
To answer your question:
component.metadata = /// whatever you want this to be
You have your error with numPages: echeq.definition.numPages. Endeed, echeq is also undefined.
You can try :
component.echeq = {
definition: {
numPages: 9
}
}
Or better way would be to returns this value from this.route.snapshot.data['submission']; so from MockActivatedRoute
UPDATE:
And update MockActivatedRoute to allows dynamic parameters:
export class MockActivatedRoute {
snapshot = {
data: {}
};
constructor(){}
withData(data:any): MockActivatedRoute {
this.snapchot.data = data;
return this;
}
}
So now in your test, you can use it :
{ provide: ActivatedRoute, useValue: new MockActivatedRoute().withData({submission:{ answers:{} } }) }
No problem. It was an easy:
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
],
imports:[
ParticipantEcheqModule,
RouterTestingModule
]
})
.compileComponents();
}));
I have component where I implement ControlValueAccessor and I'm having problems understanding the correct way to use it:
import { Component, OnInit, forwardRef, Output, EventEmitter, OnChanges, Input, ViewChild } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
import { UserOrEmail } from '../entities/UserOrEmail';
export const USER_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
// tslint:disable-next-line
useExisting: forwardRef(() => AddUserOrEmailComponent),
multi: true,
};
const noop = () => {
// Placeholder operation
};
#Component({
selector: 'app-add-user-or-email',
templateUrl: './add-user-or-email.component.html',
providers: [USER_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class AddUserOrEmailComponent implements OnInit, ControlValueAccessor {
#Input()
user: any = UserOrEmail;
#Output()
change: EventEmitter<UserOrEmail> = new EventEmitter<UserOrEmail>();
users: any = [];
ngOnInit() {
this.user = {
userId: 'ull',
name: 'null',
email: 'null'
};
this.users = ConstantService.UserArray;
}
// #region [ Value Accessor Interface ]--------------------------------------------------------
// Placeholders for the callbacks which are later provided
// by the Control Value Accessor
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;
get value(): any {
return this.user;
}
// [ ControlValueAccessor interface implementation ]-------------------------------------------
set value(v: any) {
if (this.user !== v) {
this.user = <UserOrEmail>v;
this.onChangeCallback(v);
this.change.next(this.user);
}
}
writeValue(value: any) {
if (value !== this.user)
this.user = <UserOrEmail>value;
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
and html:
<div>
<div class="form-column">
<div class="form-row">
<label>
{{'GENERIC.USER'|translate}}
</label>
<select>
<option [ngValue]="'default'"></option>
<option *ngFor="let user of users" [ngValue]="user">{{user.login}}</option>
</select>
</div>
<div class="form-row">
<label for="addPersonEmail" >{{'GENERIC.EMAIL' | translate}}</label>
<input type="email" placeholder="{{'GENERIC.EMAIL'|translate}}" pattern="^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$">
</div>
</div>
</div>
I try to use it in another component:
hmtl:
<app-modal class="form" #addMilestoneModal [width]="550">
<div header>{{'MODALS.ADD_MILESTONE'|translate}}</div>
<div body>
<div class="form-column">
<div class="form-value">
<app-add-user-or-email #addUser [(ngModel)]="milestone.assignee"></app-add-user-or-email>
</div>
<div class="form-row">
<label for="addMilestoneDescription">{{'GENERIC.DESCRIPTION' | translate}}</label>
<textarea style="height: 150px" [(ngModel)]="milestone.description"></textarea>
</div>
</div>
</div>
<div footer class="flex-container">
<button class="flex-item-row btn btn-a" [disabled]="!milestone.description" (click)="apply()">{{'GENERIC.APPLY'
| translate}}</button>
</div>
</app-modal>
Typescript:
import { Component, ViewChild, EventEmitter } from '#angular/core';
import { ModalComponent } from '../../widgets/modal/modal.component';
import { Milestone } from '../../entities/Milestone';
import { ConstantService } from '../../services/ConstantService';
import { AddUserOrEmailComponent } from '../../add-user-or-email/add-user-or-email.component';
#Component({
selector: 'app-add-milestone-modal',
templateUrl: './add-milestone-modal.component.html'
})
export class AddMilestoneModalComponent {
#ViewChild('addMilestoneModal')
modal: ModalComponent;
cs = ConstantService;
emitter: EventEmitter<Milestone> = new EventEmitter<Milestone>();
milestone: any = Milestone;
apply() {
console.log(this.milestone); // <---- HERE IT SHOULD BE ACCESSED
debugger;
this.emitter.next(this.milestone);
this.modal.close();
}
cancel() {
this.emitter.next(null);
this.modal.close();
}
}
I should get it in milestone object but it is empty. What am I missing?
I had problems with this a couple of weeks ago and made a StackBlitz with a good example.
Hopefully this can help you?
https://stackblitz.com/edit/mat-select-with-controlvalueaccessor