angulartypescriptunit-testingjasmine

Angular Unit Testing Service - Property in service constructor not using value from spyOnProperty


I'm trying to write a negative test for my AuthService that a property in the constructor is set to null if nothing is returned from angularFireAuth. The positive side of this test case is working, and if I modify my mock file directly I can see the outcomes I want. Even with the spyOnProperty mock I have below, when I subscribe to it I can see in console logs that it's returning null.

The problem is that I can't get the actual service constructor to pick up the spyOnProperty returnValue. For a component I would do a fixture.detectChanges but that doesn't work for a service. Is there some way I have to rebuild the service before the constructor picks up the mocked null value?

AuthService Constructor

{
(this.partyCollectionPath);
    this.afAuth.authState.subscribe((user: User | null) => {
      if (user) {
        this.userData = {
          uid: user.uid,
          email: user.email,
          emailVerified: user.emailVerified
        };
        this.liveUid = user.uid
      } else {
        this.userData = null
      }
    });
  }

AngularFireAuthMock

import { User } from '../shared/interface/user';
import { of } from 'rxjs';

const authState: User = {
    uid: 'fakeuser',
    email: 'test@test.com',
    emailVerified: true
};
export const AngularFireAuthMock: any = {
    AngularFireAuth: jasmine.createSpyObj('AngularFireAuth', {
//other functions
}),

get authState() {
    return of(authState);
  },

Test Assertion

  it('userData set to null', () => {

    const spy = spyOnProperty(AngularFireAuthMock, 'authState', 'get');
    spy.and.returnValue(of(null))

    AngularFireAuthMock.authState.subscribe((data: any) => {
      
      console.log('subscribe: ', data)
      
    })
    console.log('userData: ', service.userData) 
    // expect(service.userData).toBeNull()

  });

Console Logs

LOG: 'subscribe: ', null

LOG: 'userData: ', Object{uid: 'fakeuser', email: 'test@test.com', emailVerified: true}


Solution

  • This ended up being my lack of understanding on the fundamental TestBed structure for Angular tests. I finally read the Angular documentation testing services section in full, and realized that the command that essentially builds the service to test is TestBed.inject().

    By default in the generated .spec file for a service, that gets added into the beforeEach(). Essentially this meant that the constructor was being built before my spyOnProperty was even being run The solution was to take the TestBed.inject() out of the beforeEach() section, and run it AFTER my spy.

    Here is the completed solution:

    AuthService

    import { Injectable } from '@angular/core';
    import { Router } from '@angular/router';
    import { User } from '../interface/user';
    //Angularfire Imports
    import { AngularFireAuth } from '@angular/fire/compat/auth';
    import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/compat/firestore';
    @Injectable({
      providedIn: 'root'
    })
    export class AuthService {
    
      userData = {} as User | null
      public partyUserId: string = ''
      public liveUid: string = ''
    
      constructor(
        public afs: AngularFirestore,
        public afAuth: AngularFireAuth,
        public router: Router,
    
      ) {
    //This is the code I'm trying to test, I subscribe  in the constructor and  assign a property with the user data. If the response is null then I set userData to null.
        this.afAuth.authState.subscribe((user: User | null) => {
          if (user) {
            this.userData = {
              uid: user.uid,
              email: user.email,
              emailVerified: user.emailVerified
            };
            this.liveUid = user.uid
          } else {
            this.userData = null
          }
        });
      }
    }
    

    AngularFireAuthMock

    import { User } from '../shared/interface/user';
    import { of } from 'rxjs';
    
    const authState: User = {
        uid: 'fakeuser',
        email: 'test@test.com',
        emailVerified: true
    };
    
    export const AngularFireAuthMock: any = {
        AngularFireAuth: jasmine.createSpyObj('AngularFireAuth', {}),
    
        get authState() {
            return of(authState);
          },
    
        get user() {
          return this.authState;
        },
      
        set user(value: any) {
          this.authState = value;
        }
    }
    

    auth.service.spec.ts

    There are two main ways I could have done this, and the actual documentation doesn't imply that one is better than the other. Originally I just took the TestBed.inject() out of the beforeEach() so I could manually place it in each test. I think for a small service that wouldn't be too bad, but I didn't want to have to remember to inject each time so I created two describe blocks with different beforeEach(). I suppose doing it this way, I could just create an entirely different mock file I passed in for the provide useValue instead of using the spyOnProperty. If anyone has a comment on a standard, or better way of doing this let me know!

    import { TestBed } from '@angular/core/testing';
    
    import { AuthService } from './auth.service';
    import { AngularFirestore } from '@angular/fire/compat/firestore';
    import { AngularFireAuth } from '@angular/fire/compat/auth';
    import { AngularFirestoreMock } from '../../testing/mock_angularFirestore';
    import { AngularFireAuthMock } from '../../testing/mock_angularFireAuth';
    import { User } from '../interface/user';
    import { of } from 'rxjs';
    
    
    //This is the first block, where I am just using the AngularFireAuthMock file and the TestBed.inject() is in the beforeEach()
    describe('AuthService', () => {
      let service: AuthService;
    
      beforeEach(() => {
        TestBed.configureTestingModule(
          { providers: [
              { provide: AngularFirestore, useValue: AngularFirestoreMock },
              { provide: AngularFireAuth, useValue: AngularFireAuthMock }
            ]});
    
          service = TestBed.inject(AuthService);
      });
    
      it('AuthService created Successfully', () => {
        expect(service).toBeTruthy();
      });
    
      it('userData populated Successfully', () => {
        const authState: User = {
          uid: 'fakeuser',
          email: 'test@test.com',
          emailVerified: true
        };
        expect(service.userData).toEqual(authState)
        expect(service.liveUid).toEqual(authState.uid)
      });
    });
    
    //This is the second describe block where I add in the spyOnProperty right before I do the TestBed.inject(). Now, if I have other tests or assertions to run when authState or userData is null, I can write them here.
    
    describe('AuthService Null checks', () => {
      let service: AuthService;
    
      beforeEach(() => {
        TestBed.configureTestingModule(
          { providers: [
              { provide: AngularFirestore, useValue: AngularFirestoreMock },
              { provide: AngularFireAuth, useValue: AngularFireAuthMock }
            ]});
    
        spyOnProperty(AngularFireAuthMock, 'authState', 'get').and.returnValue(of(null))
    
        service = TestBed.inject(AuthService);
    
      });
    
      it('userData set to null', () => {
    
        expect(service.userData).toBeNull()
    
      });
    
    });