0

I am trying to unit test a angular 2 component and simulate an error (400/bad request) from the http backend.

Here is my code:

describe('Component: UserAccountActivationComponent', () => {

  let fixture: ComponentFixture<UserAccountActivationComponent>;
  let userAccountActivationComponent: UserAccountActivationComponent;
  const observableMock = Observable.of('Some Observable');

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      providers: [
        MockBackend,
        BaseRequestOptions,
        {
          provide: Http,
          useFactory: (backend, options) => new Http(backend, options),
          deps: [MockBackend, BaseRequestOptions]
        },
        {
          provide: ActivatedRoute, useClass: RouteMock
        },
      ],
      imports: [AppModule, HttpModule]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(UserAccountActivationComponent);
    userAccountActivationComponent = fixture.componentInstance;
  });

  it('should not signin user nor navigate to dashboard if account is not activated', fakeAsync(inject(
    [UserAccountService, SessionSigninService, Router, MockBackend],
    (userAccountService: UserAccountService, signinService: SessionSigninService, router: Router, mockBackend: MockBackend) => {

      const opts = {
        type: ResponseType.Error,
        status: 400
      };
      const responseOpts = new ResponseOptions(opts);

      mockBackend.connections.subscribe(
        (connection: MockConnection) => {
          connection.mockError(new MockError(responseOpts));
        });

      const activateSpy = spyOn(userAccountService, 'activateAccount').and.callThrough();
      const signinSpy = spyOn(signinService, 'signinByUserAccountToken').and.returnValue(observableMock);
      const navigateSpy = spyOn(router, 'navigate').and.returnValue(observableMock);

      fixture.detectChanges();
      tick();

      expect(activateSpy).toHaveBeenCalled();
      expect(signinSpy).not.toHaveBeenCalled();
      expect(navigateSpy).not.toHaveBeenCalled();
    })));

});

class MockError extends Response implements Error {
  name: any;
  message: any;
}

class RouteMock {
  readonly params = Observable.of({userAccountToken: 'a-token'});
}

I am simulating an error occurring in the userAccountService.activateAccount method. I just want to check that neither signinService.signinByUserAccountToken nor the router.navigate are called. This is what I am trying to do with the jasmine expectations:

  expect(activateSpy).toHaveBeenCalled();
  expect(signinSpy).not.toHaveBeenCalled();
  expect(navigateSpy).not.toHaveBeenCalled();

However, I the test is stopped by the simulated error:

Chrome 56.0.2924 (Mac OS X 10.12.3) Component: UserAccountActivationComponent should not signin user nor navigate to dashboard if account is not activated FAILED
    Error: Error in :0:0 caused by: Response with status: 400 null for URL: null
        at ViewWrappedError.ZoneAwareError (webpack:///~/zone.js/dist/zone.js:811:0 <- src/test.ts:142448:33)
        at ViewWrappedError.BaseError [as constructor] (webpack:///~/@angular/core/src/facade/errors.js:22:0 <- src/test.ts:36304:16)
        at ViewWrappedError.WrappedError [as constructor] (webpack:///~/@angular/core/src/facade/errors.js:87:0 <- src/test.ts:36369:16)
        at new ViewWrappedError (webpack:///~/@angular/core/src/linker/errors.js:77:0 <- src/test.ts:70405:16)
        at proxyClass.DebugAppView._rethrowWithContext (webpack:///~/@angular/core/src/linker/view.js:653:0 <- src/test.ts:111685:23)
        at proxyClass.DebugAppView.detectChanges (webpack:///~/@angular/core/src/linker/view.js:626:0 <- src/test.ts:111658:18)
        at ViewRef_.detectChanges (webpack:///~/@angular/core/src/linker/view_ref.js:170:0 <- src/test.ts:71336:20)
        at ComponentFixture._tick (webpack:///~/@angular/core/bundles/core-testing.umd.js:196:0 <- src/test.ts:15653:36)
        at webpack:///~/@angular/core/bundles/core-testing.umd.js:210:45 <- src/test.ts:15667:53
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:242:0 <- src/test.ts:141879:26)
        at ProxyZoneSpec.onInvoke (webpack:///~/zone.js/dist/proxy.js:79:0 <- src/test.ts:98996:39)
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:241:0 <- src/test.ts:141878:32)
        at Object.onInvoke (webpack:///~/@angular/core/src/zone/ng_zone.js:271:0 <- src/test.ts:38049:37)
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:241:0 <- src/test.ts:141878:32)
        at Zone.run (webpack:///~/zone.js/dist/zone.js:113:0 <- src/test.ts:141750:43)

FYI, here is the component under test:

@Component({
  templateUrl: './useraccount-activation.component.html'
})
export class UserAccountActivationComponent implements OnInit {

  constructor(private userAccountService: UserAccountService,
              private signinService: SessionSigninService,
              private router: Router,
              private route: ActivatedRoute) {
  }

  ngOnInit() {
    this.route.params
      .take(1)
      .pluck('userAccountToken')
      .switchMap((userAccountToken: string) =>
        this.userAccountService.activateAccount(userAccountToken)
          .switchMapTo(this.signinService.signinByUserAccountToken(userAccountToken))
      )
      .subscribe(() => this.router.navigate(['/dashboard']));
  }
}

edit 1: Unfortunately, changing to:

  mockBackend.connections.subscribe(
    (connection: MockConnection) => {
      responseOpts.url = connection.request.url;
      connection.mockError(new MockError(responseOpts));
    });

Still causes the following error:

'HttpClient error: ', MockError{_body: null, status: 400, ok: false, statusText: null, headers: null, type: 3, url: '/api/useraccount/activate/a-valid-token'}

    Error: Error in :0:0 caused by: Response with status: 400 null for URL: /api/useraccount/activate/a-valid-token
        at Error.ZoneAwareError (webpack:///~/zone.js/dist/zone.js:958:0 <- src/test.ts:143513:33)
        at ZoneAwareError (webpack:///~/zone.js/dist/zone.js:955:0 <- src/test.ts:143510:35)
        at wrappedError (webpack:///~/@angular/core/src/error_handler.js:144:21 <- src/test.ts:36697:34)
        at viewWrappedError (webpack:///~/@angular/core/src/linker/errors.js:65:21 <- src/test.ts:70785:125)
        at proxyClass.DebugAppView._rethrowWithContext (webpack:///~/@angular/core/src/linker/view.js:656:0 <- src/test.ts:112336:111)
        at proxyClass.DebugAppView.detectChanges (webpack:///~/@angular/core/src/linker/view.js:629:0 <- src/test.ts:112309:18)
        at ViewRef_.detectChanges (webpack:///~/@angular/core/src/linker/view_ref.js:172:0 <- src/test.ts:71715:20)
        at ComponentFixture._tick (webpack:///~/@angular/core/bundles/core-testing.umd.js:196:0 <- src/test.ts:15805:36)
        at webpack:///~/@angular/core/bundles/core-testing.umd.js:210:45 <- src/test.ts:15819:53
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:330:0 <- src/test.ts:142885:26)
        at ProxyZoneSpec.onInvoke (webpack:///~/zone.js/dist/proxy.js:79:0 <- src/test.ts:99788:39)
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:329:0 <- src/test.ts:142884:32)
        at Object.onInvoke (webpack:///~/@angular/core/src/zone/ng_zone.js:273:0 <- src/test.ts:38516:37)
        at ZoneDelegate.invoke (webpack:///~/zone.js/dist/zone.js:329:0 <- src/test.ts:142884:32)
        at Zone.run (webpack:///~/zone.js/dist/zone.js:126:0 <- src/test.ts:142681:43)
balteo
  • 23,602
  • 63
  • 219
  • 412

1 Answers1

1

It looks to me like you are getting the exception because the Url is not specified. Try setting the url on the options like so:

let responseOpts = new ResponseOptions(opts);

mockBackend.connections.subscribe(
  (connection: MockConnection) => {
    responseOpts.url = connection.request.url;
    connection.mockError(new MockError(responseOpts));
});

Edit 1

The issue you are seeing might be because the error is not being caught anywhere and so it is bubbling up to zone. Try adding an error catch to your subscribe to handle exceptions or squelch the error, (error) => {}.

this.route.params
  .take(1)
  .pluck('userAccountToken')
  .switchMap((userAccountToken: string) =>
    this.userAccountService.activateAccount(userAccountToken)
      .switchMapTo(this.signinService.signinByUserAccountToken(userAccountToken))
  )
  .subscribe(() => this.router.navigate(['/dashboard']), (error) => {});
Teddy Sterne
  • 13,774
  • 2
  • 46
  • 51
  • Hi Teddy, Unfortunately, the change you suggested does not prevent the test to be halted by the error... Any other suggestion? – balteo Feb 15 '17 at 17:59
  • Updated my answer. – Teddy Sterne Feb 15 '17 at 18:27
  • It is definitely due to the error bubbling up to zone. You're right. I tried swallowing the error as you suggested but the issue is that I am modifying production code here... I am sure there is a way to simulate the error by fixing the test itself. Furthermore, the `signinByUserAccount` is still invoked... – balteo Feb 15 '17 at 18:55
  • `signinByUserAccount` will be called everytime because of how the observable is set up. The `switchMap` and `switchMapTo` chain the observables together which means they will all be called during the observable pipeline. Furthermore, if it is not reasonable to catch the error in the production code then there is no reason to create a test that causes an error. If the method were to error when in production then the error would be propagated to the page if there was no catch for the error case. – Teddy Sterne Feb 15 '17 at 19:17
  • @balteo does the above make sense? – Teddy Sterne Feb 17 '17 at 13:22
  • Sorry I did not reply before. Yes it does make sense. I guess I need to somehow rethink error management in my app together with the way I test it. This might require opening another question. – balteo Feb 17 '17 at 17:36
  • Your reply is relevant and exact although it does not fully solve my problem. I will still upvote it and award the bounty. Thanks a lot. – balteo Feb 17 '17 at 17:38
  • Welcome. If you create a new question post a link here and I will take a look at it. – Teddy Sterne Feb 17 '17 at 17:39
  • OK. Will do so. – balteo Feb 17 '17 at 17:40