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

Time:07-16

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?

CodePudding user response:

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.

CodePudding user response:

You get the NullInjectorError: No provider for Auth! error, because the AuthService injects the private auth: Auth variable in his constructor. In your configureTestingModule only AuthService provided.

If your goal is to write a Unit test for AuthService you need to provide mocked objects for all the three dependencies injected in it, I mean for followings:

private auth: Auth,
private router: Router,
private alertController: AlertController

You can find more options about how to do this here: https://angular.io/guide/testing-services#services-with-dependencies.

For example if we would like provide a mock for the alertController the TestBed configuration would look similar to this:

// create a spy object for alertController.create() function
const alertControllerSpy = jasmine.createSpyObj('AlertController', ['create']);
TestBed.configureTestingModule({
  imports: [ 
             ReactiveFormsModule, 
             FormsModule, 
             RouterTestingModule
           ],
  providers: [
              AuthService, 
              { provide: AlertController, useValue: alertControllerSpy},
              // here be should provided a mocked object for the other depedencies
             ]
});
  • Related