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.
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();
}
}
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?
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.
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 ...
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.
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'));
});
});
});
});
…
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With