3

I have been going round in circles trying to unit test a Service (AuthService) that depends upon AngularFireAuth.

I am trying to find a way to mock, or highjack the Observable AngularFireAuth.authState instead of the Service actually talking to Firebase.

Here is my test spec:

import { inject, TestBed } 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: firebase.User = null;
const mockAngularFireAuth: any = { authState: Observable.of(authState) };

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

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

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

And here is my (simplified) Service:

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;
      }
    }, (error) => {
      throw new Error(error.message);
    });
  }

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

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

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

  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();
  }
}

I get the error Property 'authState' is private and only accessible within class 'AuthService'.

Of course it is, but I don't want to actually access it — I want to mock or highjack it so I can control it's value from within my test spec. I believe I am way off-course with my code here.

Please note I am using version ^4 of AngularFire2 and there were breaking changes introduced; documented here: https://github.com/angular/angularfire2/blob/master/docs/version-4-upgrade.md

1 Answer 1

3

Encapsulated members can be reflected.

The hard way:

expect(Reflect.get(service, 'authState')).toBe(null);

The easy way:

expect(service['authState']).toBe(null);
expect((service as any).authState).toBe(null);
Sign up to request clarification or add additional context in comments.

18 Comments

Thank you. How could I then set the authState too? Say if I wanted to test the currentUid getter in the Service. Cheers
You're welcome. Similarly, with Reflect.set(...). Or even easier, service['authState'] = ... or (service as any).authState = .... It looks like purely TS question, I would suggest to add typescript tag to the question instead of firebase since it's relevant here.
Fantastic! Just to educate myself; even though I'm no longer overriding AngularFireAuth with a mock (i.e. no longer doing { provider: AngularFireAuth, useValue: mockAngularFireAuth }) how come there are no calls to Firebase? init in the Service should be creating an anonymous user. It's not (during the test — it does when I run the app) which is great, but why not?
It's hard to say. This depends on how you actually checked that there are no calls. Were there no XHR requests? Any way, in unit tests you never need to use unmocked third-party providers (e.g. Firebase).
@PedroFerreira I'm not sure what you mean. This is not specific to Jasmine. It's how JS works, there's no difference between service.authState and service['authState'], they are evaluated exactly the same way. And FWIW, TS (service as any).authState is transpiled to JS service.authState. The question doesn't address getters, so doesn't the answer, there's spyOnProperty in Jasmine for that, and yes, it cannot be accessed as service.getter, this needs to be done another way, the most simple way is to store a spy that spyOnProperty returns to a variable.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.