angularfire icon indicating copy to clipboard operation
angularfire copied to clipboard

[docs] Mocking of angularfire methods with angularfire 7 during tests

Open alexis-mrc opened this issue 3 years ago • 4 comments

Version info

Angular: 12

Firebase: 9

AngularFire: 7

How to reproduce these conditions

How to mock firebase operator during tests suits ? How to write unit tests with mocks of firestore/auth/analytics methods ?

Expected behavior

With Angularfire 6, I was able to mock angularfire methods like doc, collection and other methods to write my unit tests. I would like to be able to mock it.

Actual behavior

I am getting the error during my unit tests : Error: Either AngularFireModule has not been provided in your AppModule (this can be done manually or implictly using provideFirebaseApp) or you're calling an AngularFire method outside of an NgModule (which is not supported).

alexis-mrc avatar Nov 08 '21 00:11 alexis-mrc

This issue does not seem to follow the issue template. Make sure you provide all the required information.

google-oss-bot avatar Nov 08 '21 00:11 google-oss-bot

So, after a year with no response, I'm sharing my workaround.

For the context, jasmine is not able to spy the functions exported by Typescript 4.

(This is possible with jest for those who use it).

For those who use jasmine, the famous wrapper technique 🤮, allows to spy and mock the functions:

import { authState, signInWithEmailAndPassword, signInWithPopup, signOut } from '@angular/fire/auth';

export abstract class AngularFireShimWrapper {
  static readonly authState = authState;
  static readonly signInWithEmailAndPassword = signInWithEmailAndPassword;
  static readonly signInWithPopup = signInWithPopup;
  static readonly signOut = signOut;
}

AuthService to test :

import { Injectable } from '@angular/core';
import { Auth, GoogleAuthProvider } from '@angular/fire/auth';
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';

import { AngularFireShimWrapper } from '~shared/utils/angular-fire-shim-wrapper';

import { User } from './user.model';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  user$: Observable<User | null>;

  constructor(private auth: Auth) {
    this.user$ = AngularFireShimWrapper.authState(this.auth).pipe(
      map((user) =>
        user !== null
          ? ({
              uid: user.uid,
              displayName: user.displayName,
              photoURL: user.photoURL,
              email: user.email,
            } as User)
          : null
      )
    );
  }

  signOut(): Observable<void> {
    return from(AngularFireShimWrapper.signOut(this.auth));
  }

  signInWithGoogle() {
    return from(AngularFireShimWrapper.signInWithPopup(this.auth, new GoogleAuthProvider()));
  }

  signInWithEmailAndPassword(email: string, password: string) {
    return from(AngularFireShimWrapper.signInWithEmailAndPassword(this.auth, email, password));
  }
}

Units tests :

import { Auth, User, UserCredential } from '@angular/fire/auth';
import { MockBuilder, ngMocks } from 'ng-mocks';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';

import { AngularFireShimWrapper } from '~shared/utils/angular-fire-shim-wrapper';
import { mockUser } from '~tests/mocks/user.spec';

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

fdescribe('AuthService', () => {
  let authService: AuthService;
  const auth = jasmine.createSpy();
  const authState$ = new BehaviorSubject<User | null>(null);

  beforeEach(async () => {
    spyOn(AngularFireShimWrapper, 'authState').and.returnValue(authState$);

    await MockBuilder(AuthService).provide({ provide: Auth, useValue: auth });

    authService = ngMocks.findInstance(AuthService);
  });

  it('should be created', () => {
    expect(authService).toBeDefined();
  });

  it('should handle null User', (done: DoneFn) => {
    authState$.next(null);

    authService.user$.pipe(first()).subscribe({
      next: (v) => {
        expect(v).toBeNull();
        done();
      },
      error: done.fail,
    });
  });

  it('should return an observable of the user', (done: DoneFn) => {
    authState$.next(mockUser as unknown as User);

    authService.user$.pipe(first()).subscribe({
      next: (v) => {
        expect(v).toEqual(mockUser);
        done();
      },
      error: done.fail,
    });
  });

  it('should call the popup opening method', (done: DoneFn) => {
    spyOn(AngularFireShimWrapper, 'signInWithPopup').and.returnValue(Promise.resolve({} as unknown as UserCredential));

    authService
      .signInWithGoogle()
      .pipe(first())
      .subscribe({
        next: () => {
          expect(AngularFireShimWrapper.signInWithPopup).toHaveBeenCalled();
          done();
        },
        error: done.fail,
      });
  });

  it('should call the popup opening method', (done: DoneFn) => {
    spyOn(AngularFireShimWrapper, 'signInWithEmailAndPassword').and.returnValue(Promise.resolve({} as unknown as UserCredential));

    authService
      .signInWithEmailAndPassword('[email protected]', '123456')
      .pipe(first())
      .subscribe({
        next: () => {
          expect(AngularFireShimWrapper.signInWithEmailAndPassword).toHaveBeenCalledWith(auth, '[email protected]', '123456');
          done();
        },
        error: done.fail,
      });
  });

  it('should call the sign method', () => {
    spyOn(AngularFireShimWrapper, 'signOut').and.returnValue(Promise.resolve());

    authService.signOut();

    expect(AngularFireShimWrapper.signOut).toHaveBeenCalled();
  });
});

giphy (1)

The logic remains the same for all functions exposed directly from version 7 of angular/fire.

kekel87 avatar Jul 26 '22 20:07 kekel87

So, after a year with no response, I'm sharing my workaround.

For the context, jasmine is not able to spy the functions exported by Typescript 4.

(This is possible with jest for those who use it).

For those who use jasmine, the famous wrapper technique 🤮, allows to spy and mock the functions:

import { authState, signInWithEmailAndPassword, signInWithPopup, signOut } from '@angular/fire/auth';

export abstract class AngularFireShimWrapper {
  static readonly authState = authState;
  static readonly signInWithEmailAndPassword = signInWithEmailAndPassword;
  static readonly signInWithPopup = signInWithPopup;
  static readonly signOut = signOut;
}

AuthService to test :

import { Injectable } from '@angular/core';
import { Auth, GoogleAuthProvider } from '@angular/fire/auth';
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';

import { AngularFireShimWrapper } from '~shared/utils/angular-fire-shim-wrapper';

import { User } from './user.model';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  user$: Observable<User | null>;

  constructor(private auth: Auth) {
    this.user$ = AngularFireShimWrapper.authState(this.auth).pipe(
      map((user) =>
        user !== null
          ? ({
              uid: user.uid,
              displayName: user.displayName,
              photoURL: user.photoURL,
              email: user.email,
            } as User)
          : null
      )
    );
  }

  signOut(): Observable<void> {
    return from(AngularFireShimWrapper.signOut(this.auth));
  }

  signInWithGoogle() {
    return from(AngularFireShimWrapper.signInWithPopup(this.auth, new GoogleAuthProvider()));
  }

  signInWithEmailAndPassword(email: string, password: string) {
    return from(AngularFireShimWrapper.signInWithEmailAndPassword(this.auth, email, password));
  }
}

Units tests :

import { Auth, User, UserCredential } from '@angular/fire/auth';
import { MockBuilder, ngMocks } from 'ng-mocks';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';

import { AngularFireShimWrapper } from '~shared/utils/angular-fire-shim-wrapper';
import { mockUser } from '~tests/mocks/user.spec';

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

fdescribe('AuthService', () => {
  let authService: AuthService;
  const auth = jasmine.createSpy();
  const authState$ = new BehaviorSubject<User | null>(null);

  beforeEach(async () => {
    spyOn(AngularFireShimWrapper, 'authState').and.returnValue(authState$);

    await MockBuilder(AuthService).provide({ provide: Auth, useValue: auth });

    authService = ngMocks.findInstance(AuthService);
  });

  it('should be created', () => {
    expect(authService).toBeDefined();
  });

  it('should handle null User', (done: DoneFn) => {
    authState$.next(null);

    authService.user$.pipe(first()).subscribe({
      next: (v) => {
        expect(v).toBeNull();
        done();
      },
      error: done.fail,
    });
  });

  it('should return an observable of the user', (done: DoneFn) => {
    authState$.next(mockUser as unknown as User);

    authService.user$.pipe(first()).subscribe({
      next: (v) => {
        expect(v).toEqual(mockUser);
        done();
      },
      error: done.fail,
    });
  });

  it('should call the popup opening method', (done: DoneFn) => {
    spyOn(AngularFireShimWrapper, 'signInWithPopup').and.returnValue(Promise.resolve({} as unknown as UserCredential));

    authService
      .signInWithGoogle()
      .pipe(first())
      .subscribe({
        next: () => {
          expect(AngularFireShimWrapper.signInWithPopup).toHaveBeenCalled();
          done();
        },
        error: done.fail,
      });
  });

  it('should call the popup opening method', (done: DoneFn) => {
    spyOn(AngularFireShimWrapper, 'signInWithEmailAndPassword').and.returnValue(Promise.resolve({} as unknown as UserCredential));

    authService
      .signInWithEmailAndPassword('[email protected]', '123456')
      .pipe(first())
      .subscribe({
        next: () => {
          expect(AngularFireShimWrapper.signInWithEmailAndPassword).toHaveBeenCalledWith(auth, '[email protected]', '123456');
          done();
        },
        error: done.fail,
      });
  });

  it('should call the sign method', () => {
    spyOn(AngularFireShimWrapper, 'signOut').and.returnValue(Promise.resolve());

    authService.signOut();

    expect(AngularFireShimWrapper.signOut).toHaveBeenCalled();
  });
});

giphy (1) giphy (1)

The logic remains the same for all functions exposed directly from version 7 of angular/fire.

Saved my day literally

docaohuynh avatar Feb 06 '23 15:02 docaohuynh

Dont know how you came up with this, but excellent job!!!

New to angular and this helped me out A LOT

Kwabena-Agyeman avatar Aug 04 '23 13:08 Kwabena-Agyeman