In my Jasmine test specs, on occasion I spy on authState
, a property of my mock service mockAngularFireAuth
and return different values representative of the state of the app under that specific test.
In one test this works perfectly and the assertion is correct; see the test:
AuthService
catastrophically fails
However, when I spy on authState
in exactly the same way in the test (for example)…
AuthService
can’t authenticate anonymously
AuthService.currentUid
should return undefined
…the assertion expect(service.currentUid).toBeUndefined()
fails.
The currentUid
remains as it was set initially, a string ("17WvU2Vj58SnTz8v7EqyYYb0WRc2").
Here is a trimmed down version of my test specs (this includes only the test specs in question):
import { async, inject, TestBed } 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';
// An anonymous user
const authState: MockUser = {
displayName: null,
isAnonymous: true,
uid: '17WvU2Vj58SnTz8v7EqyYYb0WRc2'
};
// Mock AngularFireAuth
const mockAngularFireAuth: any = {
auth: jasmine.createSpyObj('auth', {
'signInAnonymously': Promise.resolve(authState)
}),
authState: Observable.of(authState)
};
describe('AuthService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: AngularFireAuth, useValue: mockAngularFireAuth },
{ provide: AuthService, useClass: AuthService }
]
});
});
…
describe('can’t authenticate anonymously', () => {
…
describe('AuthService.currentUid', () => {
beforeEach(() => {
// const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
//
// spy.and.returnValue(Observable.of(null));
//
mockAngularFireAuth.authState = Observable.of(null);
});
it('should return undefined',
inject([ AuthService ], (service: AuthService) => {
expect(service.currentUid).toBeUndefined();
}));
});
});
describe('catastrophically fails', () => {
beforeEach(() => {
const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
spy.and.returnValue(Observable.throw(new Error('Some catastrophe')));
});
describe('AngularFireAuth.authState', () => {
it('should invoke it’s onError function', () => {
mockAngularFireAuth.authState.subscribe(null,
(error: Error) => {
expect(error).toEqual(new Error('Some catastrophe'));
});
});
});
describe('AuthService.currentUid', () => {
beforeEach(() => {
mockAngularFireAuth.authState = Observable.of(null);
});
it('should return undefined',
inject([ AuthService ], (service: AuthService) => {
expect(service.currentUid).toBeUndefined();
}));
});
});
describe('is authenticated anonymously already', () => {
beforeEach(() => {
// const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
//
// spy.and.returnValue(Observable.of(authState));
//
mockAngularFireAuth.authState = Observable.of(authState);
});
describe('.authSate.isAnonymous', () => {
it('should be true', async(() => {
mockAngularFireAuth.authState.subscribe((data: MockUser) => {
expect(data.isAnonymous).toBeTruthy();
});
}));
});
describe('AuthService.currentUid', () => {
it('should return "17WvU2Vj58SnTz8v7EqyYYb0WRc2"',
inject([ AuthService ], (service: AuthService) => {
expect(service.currentUid).toBe('17WvU2Vj58SnTz8v7EqyYYb0WRc2');
}));
});
});
describe('is authenticated with Facebook already', () => {
beforeEach(() => {
const obj: MockUser = authState;
// const spy: jasmine.Spy = spyOn(mockAngularFireAuth, 'authState');
//
// spy.and.returnValue(Observable.of(Object.assign(obj, {
// isAnonymous: false,
// uid: 'ZzVRkeduEW1bJC6pmcmb9VjyeERt'
// })));
//
mockAngularFireAuth.authState = Observable.of(Object.assign(obj, {
isAnonymous: false,
uid: 'ZzVRkeduEW1bJC6pmcmb9VjyeERt'
}));
});
describe('.authSate.isAnonymous', () => {
it('should be false', () => {
mockAngularFireAuth.authState.subscribe((data: MockUser) => {
expect(data.isAnonymous).toBe(false);
});
});
});
describe('AuthService.currentUid', () => {
it('should return "ZzVRkeduEW1bJC6pmcmb9VjyeERt"',
inject([ AuthService ], (service: AuthService) => {
expect(service.currentUid).toBe('ZzVRkeduEW1bJC6pmcmb9VjyeERt');
}));
});
});
});
You can see where I've commented out the the spies and instead had to highjack the authSate
property of mockAngularFireAuth
to get the assertions to succeed, by forcibly changing it's value — something I shouldn't be doing as mockAngularFireAuth
is a constant.
For completeness, here is (a partial of) the service under test:
import { Injectable } from '@angular/core';
import { AngularFireAuth } from 'angularfire2/auth';
import * as firebase from 'firebase/app';
import 'rxjs/add/observable/of';
// import 'rxjs/add/operator/catch';
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: firebase.User) => {
if (authState === null) {
this.afAuth.auth.signInAnonymously()
.then((authState: firebase.User) => {
this.authState = authState;
})
.catch((error: Error) => {
console.error(error);
});
} else {
this.authState = authState;
}
}, (error: Error) => {
console.error(error);
});
}
public get currentUid(): string {
return this.authState ? this.authState.uid : undefined;
}
}
Is it because the in the specs with assertions that fail I have not subscribed to authState
and therefore the spy doesn't return the respective values I've set?
I think this may be because Jasmine can't spy on properties (no my knowledge) that are not functions, or getters/setters.
But then why does the spy in
AuthService
catastrophically fails
pass?
As per my update; you can't spy on properties — only functions (or methods) and property getters and setters.
Instead, I added a setAuthState
method to mockAngularFireAuth
that can alter the value if the authState
property.
It is essentially doing exactly the same as I was, but without breaking the rules of constants in TypeScript. As it's a mock service, I don't think it matters that this additional method exists.
I am not entirely sure why the successful spec is so, however. I think it may be because the method throw
is as such a function; therefore it could become the return value of a Jasmine spy.
Here is how I changed my test:
// Mock AngularFireAuth
const mockAngularFireAuth: any = {
auth: jasmine.createSpyObj('auth', {
'signInAnonymously': Promise.resolve(authState)
}),
authState: Observable.of(authState),
setAuthState: (authState: MockUser): void => {
mockAngularFireAuth.authState = Observable.of(authState);
}
};
Note the setAuthState
.
And this is how I've changed my specs (a representative example):
describe('AuthService.currentUid', () => {
beforeEach(() => {
mockAngularFireAuth.setAuthState(null);
});
it('should return undefined',
inject([ AuthService ], (service: AuthService) => {
expect(service.currentUid).toBeUndefined();
}));
});