Search code examples
angularjasminekarma-jasminespy

Jasmine Spy not Returning Correct Value


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?

Update:

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?


Solution

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