Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock AngularFireAuth When Unit Testing an Angular Service

I have an authService which when instantiated subscribes to AngularFireAuth's Observable authState and sets the services' internal (private) property authState.

So I can unit test authService I highjack the services' internal authState with Reflect.get/set in my test specs so I can control its value.

The problem is of course authService is still subscribing to AngularFireAuth's Observable authState during its instantiation and I don't want, nor need it to.

I presume I need to mock out AngularFireAuth which fakes a subscription and doesn't actually communicate to Firebase? New to unit tests I am at a loss as to how I should do this.

auth.service.ts

import { Injectable } from '@angular/core';

import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase/app';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class AuthService {
  private authState: firebase.User;

  constructor(private afAuth: AngularFireAuth) { this.init(); }

  private init(): void {
    this.afAuth.authState.subscribe((authState) => {
      if (authState === null) {
        this.afAuth.auth.signInAnonymously()
          .then((authState) => {
            this.authState = authState;
          })
          .catch((error) => {
            throw new Error(error.message);
          });
      } else {
        this.authState = authState;
      }

      console.log(authState);
    }, (error) => {
      throw new Error(error.message);
    });
  }

  public get currentUid(): string {
    return this.authState ? this.authState.uid : undefined;
  }

  public get currentUser(): firebase.User {
    return this.authState ? this.authState : undefined;
  }

  public get currentUserObservable(): Observable<firebase.User> {
    return this.afAuth.authState;
  }

  public get isAnonymous(): boolean {
    return this.authState ? this.authState.isAnonymous : false;
  }

  public get isAuthenticated(): boolean {
    return !!this.authState;
  }

  public logout(): void {
    this.afAuth.auth.signOut();
  }
}

auth.service.spec.ts

import { async, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';

import { AngularFireModule } from 'angularfire2';
import { AngularFireAuth, AngularFireAuthModule } from 'angularfire2/auth';
import * as firebase from 'firebase/app';
import 'rxjs/add/observable/of';
// import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Rx';

import { AuthService } from './auth.service';
import { environment } from '../../environments/environment';

const authState = {
  isAnonymous: true,
  uid: '17WvU2Vj58SnTz8v7EqyYYb0WRc2'
} as firebase.User;

describe('AuthService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [AngularFireModule.initializeApp(environment.firebaseAppConfig)],
      providers: [
        AngularFireAuth,
        AuthService
      ]
    });
  });

  it('should be defined', inject([ AuthService ], (service: AuthService) => {
    expect(service).toBeDefined();
  }));

  it('.currentUser should be anonymous', inject([ AuthService ], (service: AuthService) => {
    Reflect.set(service, 'authState', authState);

    expect(service.currentUser).toBe(authState);
  }));

  it('.currentUser should be undefined', inject([ AuthService ], (service: AuthService) => {
    expect(service.currentUser).toBe(undefined);
  }));

  it('.currentUserObservable should be anonymous', inject([ AuthService ], (service: AuthService) => {
    Reflect.set(service, 'authState', authState);

    service.currentUserObservable.subscribe((value) => {
      expect(value).toBe(authState);
    });
  }));

  it('.currentUserObservable should be undefined', inject([ AuthService ], (service: AuthService) => {
    service.currentUserObservable.subscribe((value) => {
      expect(value).toBe(undefined);
    });
  }));

  it('.currentUid should be of type String', inject([ AuthService ], (service: AuthService) => {
    Reflect.set(service, 'authState', authState);

    expect(service.currentUid).toBe(authState.uid);
  }));

  it('.currentUid should be undefined', inject([ AuthService ], (service: AuthService) => {
    expect(service.currentUid).toBe(undefined);
  }));

  it('.isAnonymous should be false', inject([ AuthService ], (service: AuthService) => {
    expect(service.isAnonymous).toBe(false);
  }));

  it('.isAnonymous should be true', inject([ AuthService ], (service: AuthService) => {
    Reflect.set(service, 'authState', authState);

    expect(service.isAnonymous).toBe(true);
  }));
});

For bonus points the two excluded tests (.currentUserObservable should be anonymous and .currentUserObservable should be undefined) throw the error Error: 'expect' was used when there was no current spec, this could be because an asynchronous test timed out but only when I log to the console during authService's instantiation. I'm wondering why this would be?

like image 676
Jonathon Oates Avatar asked Jul 15 '17 18:07

Jonathon Oates


People also ask

How do you mock a component in Angular unit testing?

A mock component in Angular tests can be created by MockComponent function. The mock component respects the interface of its original component, but all its methods are dummies. To create a mock component, simply pass its class into MockComponent function.

What is mocking in unit testing Angular?

Introduction. Mocking is a great idea for testing Angular apps because it makes maintenance easier and helps reduce future bugs. There are a few complex tools, such as XUnit, for mocking an Angular CLI project. You can execute the mocking methods described in this guide only if you use vanilla Jasmine + Angular Testbed ...

Which method is used to mock the service method response in Angular?

Prefer spies as they are usually the best way to mock services. These standard testing techniques are great for unit testing services in isolation. However, you almost always inject services into application classes using Angular dependency injection and you should have tests that reflect that usage pattern.


1 Answers

I needed to create and spy on mockAngularFireAuth's authState and return an Observable which I can subscribe to and expect inside the onSuccess or onError functions, a la:

import { TestBed, async, inject } from '@angular/core/testing';

import { AngularFireAuth } from 'angularfire2/auth';
import 'rxjs/add/observable/of';
import { Observable } from 'rxjs/Rx';

import { AuthService } from './auth.service';
import { MockUser} from './mock-user';
import { environment } from '../environments/environment';

describe('AuthService', () => {
  // An anonymous user
  const authState: MockUser = {
    displayName: null,
    isAnonymous: true,
    uid: '17WvU2Vj58SnTz8v7EqyYYb0WRc2'
  };

  const mockAngularFireAuth: any = {
    auth: jasmine.createSpyObj('auth', {
      'signInAnonymously': Promise.reject({
        code: 'auth/operation-not-allowed'
      }),
      // 'signInWithPopup': Promise.reject(),
      // 'signOut': Promise.reject()
    }),
    authState: Observable.of(authState)
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AngularFireAuth, useValue: mockAngularFireAuth },
        { provide: AuthService, useClass: AuthService }
      ]
    });
  });

  it('should be created', inject([ AuthService ], (service: AuthService) => {
    expect(service).toBeTruthy();
  }));

  …

  describe('catastrophically fails', () => {
    beforeEach(() => {
      const spy = spyOn(mockAngularFireAuth, 'authState');

      spy.and.returnValue(Observable.throw(new Error('Catastrophe')));
    });

    describe('AngularFireAuth.authState', () => {
      it('should invoke it’s onError function', () => {
        mockAngularFireAuth.authState.subscribe(null,
          (error: Error) => {
            expect(error).toEqual(new Error('Catastrophe'));
          });
      });
    });
  });
  …
});
like image 186
Jonathon Oates Avatar answered Oct 03 '22 08:10

Jonathon Oates