angularfire
angularfire copied to clipboard
[docs] Mocking of angularfire methods with angularfire 7 during tests
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).
This issue does not seem to follow the issue template. Make sure you provide all the required information.
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();
});
});
The logic remains the same for all functions exposed directly from version 7 of angular/fire.
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(); }); });
The logic remains the same for all functions exposed directly from version 7 of angular/fire.
Saved my day literally
Dont know how you came up with this, but excellent job!!!
New to angular and this helped me out A LOT