Angular2 – Testing call with a debounceTime
I’m using a form control that detects changes using valueChanges
and debounceTime
. I’m writing a test that spies on itemService
to check if the update
method is being called. If I remove the debounceTime
from the form control the test works fine.
Here’s the form control in the component.
this.itemControl.valueChanges.debounceTime(300).subscribe(response => {
this.itemService.update(response);
});
Here’s the test
it('should do stuff',
inject([ItemService], (itemService) => {
return new Promise((res, rej) =>{
spyOn(itemService, 'update');
let item = {
test: 'test'
};
fixture.whenStable().then(() => {
let itemControl = new FormControl('test');
fixture.componentInstance.itemControl = itemControl;
fixture.autoDetectChanges();
fixture.componentInstance.saveItem(item);
expect(itemService.update).toHaveBeenCalled();
})}));
Here’s the component’s saveItem function
saveItem(item): void {
this.itemControl.setValue(item);
}
Like I said, if I remove debounceTime
from the form control the test executes fine, but I can’t do that. I’ve tried adding a tick()
call before the expect
call but I just get this error
Unhandled Promise rejection: The code should be running in the fakeAsync zone to call this function ; Zone: ProxyZone ; Task: Promise.then ; Value: Error: The code should be running in the fakeAsync zone to call this function Error: The code should be running in the fakeAsync zone to call this function
You should use fakeAsync() and tick(). Check out the code below (the .spec.ts file) that ran successfully on my end based on your test code in question.
Explanation of code below:
fakeAsync()
and tick()
should always be used together. You can use async()/fixtureInstance.whenStable()
together, but it is less “predictable” from a programmer’s perspective. I would recommend you to use fakeAsync()/tick()
whenever you can. You should only use async()/fixtureInstance.whenStable()
when your test code makes an XHR call (aka testing Http request).
It’s best to use fakeAsync()/tick()
when you can because you have manual control over how async code operate in your test code.
As you can see in the code below (.spec.ts file). It is very important for you to call the tick method with the method parameter 300
, tick(300)
, because the debounce value you set was 300
. If you hypothetically set your debounce value to 500
, then your tick value should be 500
in your testing code, if you want it to pass in this situation.
You will notice that if you set tick(299)
your test will fail, but that is correct because you set your debounce value to 300
. This shows you the power of using fakeAsync()/tick()
, you control your codes timing (you are MASTER OF TIME, when you use fakeAsync()/tick()
).
// component.sandbox.spec.ts
import { async, TestBed, fakeAsync, tick, inject } from "@angular/core/testing";
import { ReactiveFormsModule } from "@angular/forms";
import { SandboxComponent } from "./component.sandbox";
import { ItemService } from "../../Providers";
import "rxjs/add/operator/debounceTime";
describe("testFormControl", () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [SandboxComponent],
providers: [ItemService],
}).compileComponents();
}));
// The test you had questions about :)
it("(fakeAsync usage) Should hit the ItemService instance's 'update' method once", fakeAsync(inject([ItemService], (itemService: ItemService) => {
spyOn(itemService, "update");
let fixture = TestBed.createComponent(SandboxComponent);
fixture.detectChanges(); // It is best practices to call this after creating the component b/c we want to have a baseline rendered component (with ng2 change detection triggered) after we create the component and trigger all of its lifecycle events of which may cause the need for change detection to occur, in the case attempted template data bounding occurs.
let componentUnderTest = fixture.componentInstance;
componentUnderTest.saveItem("someValueIWantToSaveHEHEHE");
tick(300); // avoliva :)
expect(itemService.update).toHaveBeenCalled();
})));
});
// component.sandbox.ts
import { Component, OnInit } from "@angular/core";
import { FormGroup, FormControl } from "@angular/forms";
import { ItemService } from "../../Providers";
@Component({
template: `
<form [formGroup]="formGroupInstance">
<input formControlName="testFormControl" />
<button type="submit">Submit</button>
<button type="button" (click)="saveItem(formGroupInstance.controls['testFormControl'].value)">saveItem(...)</button>
</form>
`,
styleUrls: ["component.sandbox.scss"],
})
export class SandboxComponent extends OnInit {
public formGroupInstance: FormGroup;
public testFormControlInstance: FormControl;
constructor(private itemService: ItemService) {
super();
this.testFormControlInstance = new FormControl();
this.formGroupInstance = new FormGroup(
{
testFormControl: this.testFormControlInstance,
},
);
}
public ngOnInit() {
this.testFormControlInstance.valueChanges
.debounceTime(300) // avoliva
.subscribe((formControlInstanceValue: {}) => {
this.itemService.update(formControlInstanceValue);
});
}
public saveItem(item: any) {
this.testFormControlInstance.setValue(item);
}
}
// ../../Provider/index.ts
export class ItemService {
public update(formControlInstanceValue: any) {
// Makes http request to api to update item
console.log(`HEY PROGRAMMER, YEAH YOU! :P \n => http request could have been made
here to update an 'item' in the database.`);
}
}