Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 519
avoliva Avatar asked Jan 13 '17 19:01

avoliva


People also ask

What is the purpose of the fixture detectChanges () call in this unit test?

Fixtures have access to a debugElement , which will give you access to the internals of the component fixture. Change detection isn't done automatically, so you'll call detectChanges on a fixture to tell Angular to run change detection.

Does fixture detectChanges call ngOnInit?

fixture. detectChanges() tells Angular to run change-detection. Finally! Every time it is called, it updates data bindings like ng-if, and re-renders the component based on the updated data. Calling this function will cause ngOnInit to run only the first time it is called.

What is tick in fakeAsync?

ticklink. Simulates the asynchronous passage of time for the timers in the fakeAsync zone.


1 Answers

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.`);
  }
}
like image 63
Steven Avatar answered Oct 22 '22 01:10

Steven