I have started learning how to test angular projects. So far basic unit testing is working fine for me but for the dependency testing especially when API services are injected into the component I am facing issue for providing HttpClient
. I have tried different solutions but none is working for me.
Service
// Present in HttpClientService file
getDisposition() {
return this.http.get<{ message: string, data: { dispositionList: Disposition[] } }>(`${this.URL}/disposition/get`);
}
// Present in FileProcessService file
deleteMedia(media: string) {
return this.http.delete<{ message: string }>(`${this.URL}/certificate/delete?certificate=${media}`);
}
add-edit-activity.component.ts
import { HttpEventType } from '@angular/common/http';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { AssignedPerson } from '@model/assigned-person.model';
import { Disposition } from '@model/disposition.model';
import { mimeTypes } from '@model/mime-type';
import { FileProcessService } from '@service/file-process.service';
import { HttpClientService } from '@service/http-client.service';
import { DeleteComponent } from '@shared/delete/delete.component';
import { CustomErrorStateMatcher } from '@validator/error-state-matcher';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-add-edit-activity',
templateUrl: './add-edit-activity.component.html',
styleUrls: ['./add-edit-activity.component.css']
})
export class AddEditActivityComponent implements OnInit {
constructor(private fb: FormBuilder, private sanitizer: DomSanitizer, private dialogRef: MatDialogRef<AddEditActivityComponent>, @Inject(MAT_DIALOG_DATA) public data: any,
private _http: HttpClientService, private _fileService: FileProcessService, private toastr: ToastrService, private dialog: MatDialog,) { }
re = new RegExp(/^[a-zA-Z-]*/, 'g');
ISOstamp = { T: ' ', Z: '000' };
matcher = new CustomErrorStateMatcher();
dispositionList: Disposition[] = [];
assignedPersonList: AssignedPerson[] = [];
filterAssignedPersonList: AssignedPerson[] = [];
uploaded = false;
uploadProgress = false;
fileURL: SafeUrl;
activityForm = this.fb.group({
gaugeId: [this.data.gaugeId, Validators.maxLength(9)], createTimeStamp: [{ value: new Date().toISOString().replace(/[TZ]/g, m => this.ISOstamp[m]), disabled: true }],
user: [{ value: sessionStorage.getItem('username'), disabled: true }], disposition: ['', [Validators.required, Validators.maxLength(30)]],
assignedPersonName: ['', Validators.maxLength(30)], department: ['', Validators.maxLength(20)],
shift: ['', Validators.maxLength(1)], remark: ['', Validators.maxLength(50)],
calibrationDate: ['', Validators.maxLength(10)], attachment: ['', Validators.maxLength(255)]
});
@ViewChild('file') certificate: ElementRef;
ngOnInit(): void {
this.data.type.match(this.re)[0] === 'Update' && this.setFormValues();
this._http.getDisposition().subscribe(response => this.dispositionList = response.data.dispositionList);
this._http.getAssignedPerson().subscribe(response => this.assignedPersonList = this.filterAssignedPersonList = response.data.assignedToList);
}
get GaugeId() {
return this.activityForm.get('gaugeId');
}
get TimeStamp() {
return this.activityForm.get('createTimeStamp');
}
get Attachment() {
return this.activityForm.get('attachment');
}
get Disposition() {
return this.activityForm.get('disposition');
}
get DispositionValue() {
return this.dispositionList.map(e => e.dispositionType).indexOf(this.Disposition.value) < 0;
}
get AssignedTo() {
return this.activityForm.get('assignedPersonName');
}
get AssignedToValue() {
return this.assignedPersonList.map(e => `${e.firstName} ${e.lastName}`).indexOf(this.Disposition.value) < 0;
}
private async setFormValues() {
this.activityForm.patchValue({ ...this.data });
if (this.data.attachment) {
this.uploadProgress = true;
this.uploaded = true;
await this.fetchUploadedFile(this.data.attachment, mimeTypes[this.data.attachment.split('.')[1]]);
this.activityForm.markAsPristine();
}
}
searchAssignedPerson(event) {
if (event.target.value) {
this.filterAssignedPersonList = [];
for (let person of this.assignedPersonList) {
if (person.firstName.toLowerCase().startsWith(event.target.value.toLowerCase())) {
this.filterAssignedPersonList.push(person);
}
}
} else { this.filterAssignedPersonList = this.assignedPersonList }
}
upload(event) {
const file: File = event.target.files[0];
this.certificate.nativeElement.value = '';
if (file.size > (20 * 1000 * 1000)) { // Checking if File size is above 20MB
this.toastr.error('Size of ' + file.name + ' is above 20MB');
return;
}
const fd = new FormData();
fd.append('certificate', file, file.name);
this.processAttachment(fd);
}
private processAttachment(file: FormData) {
this._fileService.uploadMedia(file).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) { this.uploadProgress = true }
if (event.type === HttpEventType.Response) {
let media = event.body.data.Certificate;
this.fetchUploadedFile(media.fileName, media.fileType);
this.toastr.info(event.body.message);
}
}, error => {
this.toastr.error(error.error.message);
this.uploadProgress = false;
});
}
private async fetchUploadedFile(file: string, mimeType: string) {
try {
let response = await this._fileService.getMedia(file).toPromise();
this.fileURL = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(new Blob([response], { type: mimeType })));
this.uploaded = true;
this.uploadProgress = false;
this.activityForm.patchValue({ attachment: file });
this.Attachment.markAsDirty();
} catch (error) {
this.uploadProgress = false;
this._fileService.processError(error.error)
}
}
deleteFile() {
this.dialog.open(DeleteComponent, {
width: '350px', disableClose: true
}).afterClosed().subscribe(response => {
if (response) {
this._fileService.deleteMedia(this.Attachment.value).subscribe(response => {
this.toastr.info(response.message);
this.activityForm.patchValue({ attachment: '' });
this.Attachment.markAsDirty();
this.uploaded = false;
}, error => this.toastr.error(error.error.message));
}
});
}
async doAction() {
let message = '';
if (this.data.type.match(this.re)[0] === 'Add') {
message = await (await this._http.addActivityLog(this.activityForm.getRawValue()).toPromise()).message;
} else {
message = await (await this._http.updateActivityLog(this.GaugeId.value, this.TimeStamp.value, this.activityForm.getRawValue()).toPromise()).message;
}
this.dialogRef.close(message);
}
}
add-edit-activity.component.spec.ts
import { ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FileProcessService } from '@service/file-process.service';
import { HttpClientService } from '@service/http-client.service';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { AddEditActivityComponent } from './add-edit-activity.component';
describe('AddEditActivityComponent', () => {
let component: AddEditActivityComponent;
let fixture: ComponentFixture<AddEditActivityComponent>;
let _http: HttpClientService, _file: FileProcessService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddEditActivityComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { type: 'Add Activity Log', gaugeId: 'W-001' } },,
{ provider: HttpClientService, useValue: null },
{ provider: FileProcessService, useValue: null }
]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddEditActivityComponent);
component = fixture.debugElement.componentInstance;
_http = fixture.debugElement.injector.get(HttpClientService);
_file = fixture.debugElement.injector.get(FileProcessService);
fixture.detectChanges();
});
// These 2 tests throwing error.
it('SHOULD mock http service', () => {
let spy = spyOn(_http, 'getDisposition').and.callFake(() => {
return of({
message: '',
data: { dispositionList: [] }
}).pipe(delay(100));
});
component.ngOnInit();
tick(100);
expect(component.dispositionList).toEqual([]);
});
it('SHOULD mock file service', () => {
let spy = spyOn(_file, 'deleteMedia').and.callFake((file: string) => {
return of({ message: '' }).pipe(delay(100));
});
component.deleteFile();
tick(100);
expect(component.uploaded).toBe(false);
});
});
The error that am getting for those 2 tests (I’m providing the error of 1 test case, the same is coming for the 2nd one also):
- SHOULD mock http service
AddEditActivityComponent
Error: Invalid provider for the NgModule ‘DynamicTestModule’ – only instances of Provider and Type are allowed, got: […, …, …,
…, ?undefined?, …, …]
at throwInvalidProviderError (node_modules/@angular/core/ivy_ngcc/fesm2015/core.js:240:1)
at providerToFactory (node_modules/@angular/core/ivy_ngcc/fesm2015/core.js:11550:1)
at providerToRecord (node_modules/@angular/core/ivy_ngcc/fesm2015/core.js:11521:1)
at R3Injector.processProvider (node_modules/@angular/core/ivy_ngcc/fesm2015/core.js:11424:1)
Error: : could not find an object to spy upon for getDisposition() Usage: spyOn(, ) Error: :
could not find an object to spy upon for getDisposition() Usage:
spyOn(, )
at
Anyone, please help what’s the exact error happening with my test cases? Stuck here for almost 2 days.