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
]
});