Search code examples
javascriptangularunit-testingjasmine

How do I know what should be added as a provider or import in my Jasmine Unit Test


I'm new to unit tests and I haven't been able to find a clear and concise explanation of what gets added as a provider vs import in my spec.ts. I keep getting the same error no matter what I try.

Here's my service class:

import { Injectable } from '@angular/core';
import { Auth, createUserWithEmailAndPassword, signInWithEmailAndPassword, UserCredential } from '@angular/fire/auth';
import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { signOut } from '@firebase/auth';
import { AlertController } from '@ionic/angular';
import { getFirestore, collection, addDoc } from 'firebase/firestore';
import { getApp } from 'firebase/app';
import { DocumentData, DocumentReference } from '@angular/fire/firestore';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  userCollection = collection(getFirestore(getApp()), 'users');

  constructor(private auth: Auth,
    private router: Router,
    private alertController: AlertController) { }

  async register(registrationForm: FormGroup): Promise<UserCredential> {
    return await createUserWithEmailAndPassword(this.auth, registrationForm.value.email, registrationForm.value.password);
  }

  async login(email: string, password: string): Promise<UserCredential> {
    return await signInWithEmailAndPassword(this.auth, email, password);
  }

  logout(): Promise<void> {
    return signOut(this.auth);
  }

  async authenticationAlert(message: any, header: string, route: string): Promise<void> {
    const alert = await this.alertController.create({
      subHeader: `${header}`,
      message: `${message}`,
      buttons: [
        {
          text: 'Ok',
          role: 'Ok',
          cssClass: 'secondary',
          handler: () => {
            this.router.navigateByUrl(`${route}`);
          }
        }
      ]
    });

    await alert.present();
  }

  saveProfile(uid: string, email: string, displayName: string): Promise<DocumentReference<DocumentData>> {
    return addDoc(this.userCollection, {uid, email, displayName});
  }
}

Here's the spec:

import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { AuthService } from './auth.service';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { getApp } from '@angular/fire/app';
import { signOut } from '@angular/fire/auth';
import { addDoc, collection, DocumentReference, getFirestore } from '@angular/fire/firestore';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ ReactiveFormsModule, FormsModule, RouterTestingModule,],
      providers: [AuthService]
    });
    service = TestBed.inject(AuthService);
  });

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

And this is the error I keep getting:

AuthService > should be created
NullInjectorError: R3InjectorError(DynamicTestModule)[AuthService -> Auth -> Auth]: 
  NullInjectorError: No provider for Auth!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'AuthService', 'Auth', 'Auth' ] })
NullInjectorError: R3InjectorError(DynamicTestModule)[AuthService -> Auth -> Auth]: 
  NullInjectorError: No provider for Auth!
    at NullInjector.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:9081:27)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:9248:33)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:9248:33)
    at injectInjectorOnly (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:4868:33)
    at ɵɵinject (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:4872:12)
    at Object.factory (ng:///AuthService/ɵfac.js:4:39)
    at R3Injector.hydrate (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:9343:35)
    at R3Injector.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:9237:33)
    at NgModuleRef.get (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/core.mjs:22399:33)
    at TestBedRender3.inject (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2020/testing.mjs:26537:52)
Expected undefined to be truthy.
Error: Expected undefined to be truthy.
    at <Jasmine>
    at UserContext.apply (http://localhost:9876/_karma_webpack_/webpack:/src/app/services/auth.service.spec.ts:22:21)
    at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone.js:409:30)
    at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-testing.js:303:43)

Additionally, I'm getting this error in ever spec.ts on every component that imports the AuthService.

I'm not sure how to determine out of all of my imports which ones I'll have to include in the spec file. And how do I determine if they get injected as a provider or import?


Solution

  • What gets added to imports are modules and what gets added to providers are services. Some modules export a provider/service in their exports field and therefore you can add a module to the imports section and then be able to inject the exported service in the constructor and Angular will know how to construct it.

    The issue you're facing is that Angular does not know how to create Auth and AlertController injected into the service. It knows how to create Router because of the RouterTestingModule.

    For a quick unblock, try this:

    describe('AuthService', () => {
      let service: AuthService;
      // create mocks
      let mockAuth: jasmine.SpyObj<Auth>;
      let mockAlertController: jasmine.SpyObj<AlertController>;
    
      beforeEach(() => {
        // create spy objects and add public methods in the 2nd argument as a string
        mockAuth = jasmine.crateSpyObj<Auth>('Auth', {}, {});
        mockAlertController = jasmine.createSpyObj<AlertController>('AlertController', ['create']);
        TestBed.configureTestingModule({
          imports: [ ReactiveFormsModule, FormsModule, RouterTestingModule,],
          providers: [
            AuthService,
            // give the fake services for the real ones
            { provide: Auth, useValue: mockAuth },
            { provide: AlertController, useValue: mockAlertController }
          ]
        });
        service = TestBed.inject(AuthService);
      });
    
      it('should be created', () => {
        expect(service).toBeTruthy();
      });
    });
    

    Check out this resource: https://testing-angular.com/testing-components-depending-on-services/#testing-components-depending-on-services. There is also a section of Testing Services but basically you need to mock all external dependencies.