7

I am writing unit test for Angular app that deletes account from database. To do this, I click on delete button. Then function is called on .ts file. This will delete the account by calling API.

I want to write unit test to see if this API is called or not using HttpTestingModule,which it does, but in the code after account deleted,I navigate to different page using router.navigate. When code hits it complains WARN: 'Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?'

ERROR: 'Unhandled Promise rejection:', 'Cannot match any routes. URL Segment: 'accountsList'', '; Zone:', 'ProxyZone', '; Task:', 'Promise.then', '; Value:', Error: Cannot match any routes. URL Segment: 'accountsList'
Error: Cannot match any routes. URL Segment: 'accountsList'
    at ApplyRedirects.noMatchError (./node_modules/@angular/router/fesm5/router.js?:1455:16)
    at CatchSubscriber.eval [as selector] (./node_modules/@angular/router/fesm5/router.js?:1436:29)
    at CatchSubscriber.error (./node_modules/rxjs/_esm5/internal/operators/catchError.js?:40:31)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at ThrowIfEmptySubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26), 'Error: Cannot match any routes. URL Segment: 'accountsList'
    at ApplyRedirects.noMatchError (./node_modules/@angular/router/fesm5/router.js?:1455:16)
    at CatchSubscriber.eval [as selector] (./node_modules/@angular/router/fesm5/router.js?:1436:29)
    at CatchSubscriber.error (./node_modules/rxjs/_esm5/internal/operators/catchError.js?:40:31)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at MapSubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)
    at MapSubscriber.Subscriber.error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:69:18)
    at ThrowIfEmptySubscriber.Subscriber._error (./node_modules/rxjs/_esm5/internal/Subscriber.js?:89:26)'

here is what I tried

test.spec.ts

 it ('should delete account, if account exist', ()=> {
    let spyOnDelete = spyOn (component,'deleteRecord').and.callThrough();
    fixture.detectChanges();
    component.record.accountid = "Account1";
    fixture.detectChanges();
    let deleteButtonDOM = fixture.debugElement.query(By.css('#deletebtn'));
    console.log(deleteButtonDOM.nativeElement);
    deleteButtonDOM.triggerEventHandler('click',null);
    fixture.detectChanges();
    expect(spyOnDelete).toHaveBeenCalled();  //test passes 

    const req = _HttpTestingController.expectOne('/api/accounts/'+component.record.accountid);//test fails
    expect(req.request.method).toBe("DELETE");
    req.flush({status:"SUCCESS"});
    expect(component.consoleMessages.includes("POST: SUCCESS in /api/accounts")).toBe(true);


..what must be written to 
  })

test.component.ts

  deleteRecord(id) {
    this.spinner.show();
    this.http.delete('/api/accounts/' + id)
    .subscribe(res => {
        this.spinner.hide();
        if (res['status'] == "FAILURE") {
          this.consoleMessages += "\nPOST: ERROR in /api/accounts\n" + JSON.stringify(res);  
        } else {
          this.consoleMessages += "\nPOST: SUCCESS in /api/accounts\n" + JSON.stringify(res);
          this.router.navigate(['/accountsList']);//this is creating the problem xxxxxxxxxxxxxxxx
        }
      }, (err) => {
        this.consoleMessages += "\nPOST: ERROR in /api/accounts\n" + JSON.stringify(err); 
        this.spinner.hide();
        console.log(err);
      }
    );
  }
karansys
  • 2,449
  • 7
  • 40
  • 78

2 Answers2

11

You should mock your router.

The way I do it with RouterTestingModule. Here's the set up:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [
      RouterTestingModule.withRoutes([
        { path: "", component: AccountComponent },
        { path: "**", redirectTo: "" }
      ])
    ],
    declarations: [AccountComponent],
    providers: []
  }).compileComponents();
}));

beforeEach(() => {
  fixture = TestBed.createComponent(AccountComponent);
  component = fixture.componentInstance;
  router = TestBed.inject(Router);
  fixture.detectChanges();
});

Then in your test:

it('THEN: should navigate to /accounts', () => {
      const ID_TO_DELETE = 1;
      const routerNavigateSpy = jest
        .spyOn(router, 'navigate')
        .mockImplementation(() => of(true).toPromise());
      component.deleteRecord(ID_TO_DELETE);
      expect(routerNavigateSpy).toHaveBeenCalledWith(['/accounts']);
});

Bonus

This post explains how Angular zones work: ngZone

adrisons
  • 3,443
  • 3
  • 32
  • 48
2

Minimal spy and solution for the warning router.navigate

describe('Component', () => {
  ... other variables
  let router: jasmine.SpyObj<Router>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([yourPaths])
      ],
      ...other declarations
    })
      .compileComponents();
  }));

  beforeEach(() => {
    ...other initializing stuffs
    router = TestBed.get(Router);
  });

  it('should navigate', () => {
     spyOn(router, 'navigate');
     component.navigateToRoute();
     expect(router.navigate).toHaveBeenCalledWith(['/expected-path']);
  });
});
Bhavin
  • 970
  • 1
  • 13
  • 20