Sebastien Sebastien - 1 month ago 9
TypeScript Question

Testing that a component behaves properly when the service it calls throws an exception with Angular 2

In my Angular 2 application, I'm trying to unit test the following component:

export class LoginComponent implements OnInit {
invalidCredentials = false;
unreachableBackend = false;

constructor(private authService: AuthService) {}

ngOnInit() {
this.invalidCredentials = false;
}

onSubmit(user: any) {
this.authService.authenticateUser(<User>user).subscribe((result) => {
if (!result) {
this.invalidCredentials = true;
}
}, (error) => {
if (error instanceof InvalidCredentialsError) {
this.invalidCredentials = true;
} else {
this.unreachableBackend = true;
}
});
}
}


I have already successfully tested the happy path. Now I would like to check that when authService.authenticateUser() throws an error, invalidCredentials and unreachableBackend are correctly set. Here is what I am trying:

describe('Authentication triggering an error', () => {
class FakeAuthService {
authenticateUser(user: User) {
throw new InvalidCredentialsError();
}
}

let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let authService: AuthService;
let spy: Spy;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
providers: [
{provide: AuthService, useClass: FakeAuthService},
{provide: TranslateService, useClass: FakeTranslateService}
],
imports: [ FormsModule, TranslateModule, AlertModule ]
});

fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
authService = fixture.debugElement.injector.get(AuthService);
spy = spyOn(authService, 'authenticateUser').and.callThrough();
});

it('should not log in successfully if authentication fails', () => {
const user = {username: 'username', password: 'password'};
component.onSubmit(user);

expect(authService.authenticateUser).toHaveBeenCalledWith(user);
expect(spy.calls.count()).toEqual(1, 'authenticateUser should have been called once');
expect(component.invalidCredentials).toBe(true, 'credentials should be invalid because of the exception');
expect(component.unreachableBackend).toBe(false, 'backend should be reachable at first');
});
});


But when I run this test, I get the following failure:


PhantomJS 2.1.1 (Mac OS X 0.0.0) Component: Login Authentication triggering an error should not log in successfully if authentication fails FAILED
[object Object] thrown in src/test.ts (line 49782)
authenticateUser@webpack:///Users/sarbogast/dev/unbox/frontend/src/app/login/login.component.spec.ts:112:44 <- src/test.ts:49782:67
onSubmit@webpack:///Users/sarbogast/dev/unbox/frontend/src/app/login/login.component.ts:9:4896 <- src/test.ts:85408:5955
webpack:///Users/sarbogast/dev/unbox/frontend/src/app/login/login.component.spec.ts:139:25 <- src/test.ts:49806:31
invoke@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/zone.js:203:0 <- src/test.ts:84251:33
onInvoke@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/proxy.js:72:0 <- src/test.ts:59204:45
invoke@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/zone.js:202:0 <- src/test.ts:84250:42
run@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/zone.js:96:0 <- src/test.ts:84144:49
webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/jasmine-patch.js:91:27 <- src/test.ts:58940:53
execute@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/jasmine-patch.js:119:0 <- src/test.ts:58968:46
execute@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/jasmine-patch.js:119:0 <- src/test.ts:58968:46
invokeTask@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/zone.js:236:0 <- src/test.ts:84284:42
runTask@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/zone.js:136:0 <- src/test.ts:84184:57
drainMicroTaskQueue@webpack:///Users/sarbogast/dev/unbox/frontend/~/zone.js/dist/zone.js:368:0 <- src/test.ts:84416:42
PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 33 of 38 (1 FAILED) (skipped 5) (0.704 secs / 0.842 secs)


So obviously there is something I didn't get. I should mention the fact that I'm completely new to JS unit testing and somewhat new to reactive programming.

Answer

You can't do that as the error will just be thrown and bubble up to the test. What you need to do is allow the user the subscribe with callbacks, and then you can call the error callback. For example

class FakeAuthService {
  authenticateUser(user: User) {
    // return this service so user can call subscribe
    return this;
  }

  subscribe(onNext, onError) {
    if (onError) {
      onError(new InvalidCredentialsError());
    }
  }
}

Another option is to use Observable.throw(new InvalidCredentialsError())

authenticateUser(user: User) {
  return Observable.throw(new InvalidCredentialsError());
}

This will cause any subscriber's onError callback to be called. Personally I like using the mock returning itself. I can make the mock more configurable to make testing easier. See the link below for an example of what I mean.

See also:

Comments