Search code examples
angulartypescriptunit-testingangularfire2

Mocking AngularFireAuth for angular service unit testing


I created a service to handle all of my authentication calls to angularFireAuth, but the basic toBeTruthy assertion is hitting an injector error for angularFireAuth. I solved this for several other components by creating simple mocks, but now that I've moved on to the actual service the more complicated mocking for angularFireAuth is causing me issues and I can't get through this Injector error.

The original error:

NullInjectorError: R3InjectorError(DynamicTestModule)[AuthService -> AngularFireAuth -> InjectionToken angularfire2.app.options -> InjectionToken angularfire2.app.options]:

auth.service.spec.ts

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';

describe('AuthService', () => {
  let service: AuthService;

  beforeEach(() => {
    TestBed.configureTestingModule(
      {
        providers: [
          {provide: AngularFirestore, useValue: AngularFirestoreMock},
          {provide: AngularFireAuth, useVallue: AngularFireAuthMock}
        ]

      });
    service = TestBed.inject(AuthService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

I have a simple .set function form AngularFirestore that is used to write out some extra user data during signup. I got through that injection error by providing a mock version of the service:

AngularFirestoreMock

export const AngularFirestoreMock = {

  set: () => {
 
      return
  }
};

I solved other components calling my AuthService with a similar provider override with this mock:

AuthServiceMock

export const AuthServiceMock = jasmine.createSpy('userData')
  .and.returnValue(Promise.resolve({
    uid: 'fakeuser',
    email: '[email protected]',
    emailVerified: true
  }));

One of the issues is that my AuthService ends up calling multiple different AngularFireAuth functions/methods. I went to the original documentation to see exactly what was expected for the return values for each of them:

authState.subscribe(): Promise < UserCredential >

createUserWithEmailAndPassword: Promise < UserCredential >

sendEmailVerification: Promise < void > (with potential error codes)

signOut: Promise < void >

signInWithEmailAndPassword: Promise < UserCredential >

sendPasswordResetEmail: Promise < void > (with potential error codes)

currentUser: this is a method, and I'm not totally sure if this needs to be mocked

I've tried a couple of different implementations, but at this point I'm just going in circles and I'm clearly out of my depth.

My current attempt, I know that the authState or currentUser probably isn't right, and I'm not sure if i should be using createSpyObj or not.

export const AngularFireAuthMock = jasmine.createSpyObj('AngularFireAuth', {
    'authState': {
        function() {
            return Promise.resolve(
                {
                    uid: 'fakeuser',
                    email: '[email protected]',
                    emailVerified: true

                }
            )
        }
    },

    'createUserWithEmailAndPassword': {
        function() {
            return Promise.resolve({
                credential: "AuthCredential",
                user: "User"
            })
        }
    },

    'signout': {
        function() {
            return Promise
        }
    },

    'signInWithEmailAndPassword': {
        function() {
            return Promise.resolve({
                credential: "AuthCredential | null",
                user: "User | null"
            })
        }
    },

    'sendPasswordResetEmail': {
        function() {
            return Promise.resolve({
                code: 'auth/user-not-found'
            })
        }
    },

    'sendEmailVerification': {
        function() {
            return Promise.resolve({
                code: 'auth/user-not-found'
            })
        }
    },

    'currentUser': {
        function() {
            return Promise.resolve({
                uid: 'fakeuser',
                    email: '[email protected]',
                    emailVerified: true
            })
        }
    }
},


)

Solution

  • A big thanks to @AliF50 for pointing out the typo that was stopping my provider mock for AngularFireAuth from working. After I fixed that, I ended up with a few revelations and figured out a solution, so I wanted to share an overall answer in case it helps anyone else.

    For JUST the 'is it created' unit test, I didn't need to mock out all of the various functions. I'll use those later as I build out actual unit tests, but for my strict question I only really needed to solve the mock authState.subscribe() that I call in the constructor of the service itself.

    I found this thread from 2017 that got me most of the way, I just had to slightly change the syntax and it worked. My basic unit test passed.

    Mock AngularFireAuth When Unit Testing an Angular Service

    AngularFireAuthMock (minimum required for 'is it created')

    import { User } from '../shared/interface/user';
    import { of } from 'rxjs';
    
    const authState: User = {
        uid: 'fakeuser',
        email: '[email protected]',
        emailVerified: true
    };
    
    export const AngularFireAuthMock: any = {
    
        authState: of(authState)
    }
    

    updated auth.service.spec.ts file (spelling mistake corrected)

    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';
    
    describe('AuthService', () => {
      let service: AuthService;
    
      beforeEach(() => {
        TestBed.configureTestingModule(
          {
            providers: [
              {provide: AngularFirestore, useValue: AngularFirestoreMock},
              {provide: AngularFireAuth, useValue: AngularFireAuthMock}
            ]
    
          });
        service = TestBed.inject(AuthService);
      });
    
      it('should be created', () => {
        expect(service).toBeTruthy();
      });
    });