Search code examples
angularjasmineionic4karma-jasmine

Signin Page showing no Storage provider for custom AuthService Karma


I'm finding it hard to debug why my ionic page is complaining that the AuthService does not have a Storage Provider. I tried creating a new AuthService in the TestBed but no dice. Could you also explain why this is an issue so I can learn from it? I can't find anything about Injecting a service which is injecting the storage module.

Failed: StaticInjectorError(DynamicTestModule)[AuthService -> Storage]: StaticInjectorError(Platform: core)[AuthService -> Storage]: NullInjectorError: No provider for Storage!

import {
  async,
  ComponentFixture,
  TestBed,
  fakeAsync,
  tick,
} from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { SignInPage } from './sign-in.page';
import { User } from 'src/app/classes/user';
import { AuthService } from '../../services/auth-service/auth.service';
import { UsersDataPublic } from '../../services/users-service/users-data-public';
import { UserProfileService } from '../../services/user-profile/user-profile.service';
import { UserProfileDatabase } from '../../services/user-profile/user-profile-db.service';
import { AppConstants } from '../../constants/app.constants';
import { Storage } from '@ionic/Storage';
import { NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA, inject } from '@angular/core';

const storageIonicMock: any = {
  get: () => new Promise<any>((resolve, reject) => resolve('As2342fAfgsdr')),
  set: () => new Promise<any>((resolve, reject) => resolve('As2342fAfgsdr'))
};

describe('SignInPage', () => {
  let component: SignInPage;
  let httpTestingController: HttpTestingController;
  let fixture: ComponentFixture<SignInPage>;
  let httpClient: HttpClient;
  let service: UsersDataPublic;
  let userProfileservice: UserProfileService;
  let authService: AuthService;
  let auth;
  let userProfileDatabase: UserProfileDatabase;
  let constants: AppConstants;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [SignInPage],
      providers: [
        UsersDataPublic,
        UserProfileService,
        UserProfileDatabase,
        AppConstants,
        AuthService,
        {
          provide: Storage,
          useValue: storageIonicMock
        }
      ],
      imports: [
        IonicModule.forRoot(),
        ReactiveFormsModule,
        FormsModule,
        RouterTestingModule,
        FormsModule,
        HttpClientTestingModule,
      ],
      schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA],
    }).compileComponents();

    fixture = TestBed.createComponent(SignInPage);
    constants = TestBed.get(AppConstants);
    service = TestBed.get(UsersDataPublic);
    userProfileservice = TestBed.get(UserProfileService);
    authService = TestBed.get(new AuthService(storageIonicMock, httpClient, constants));
    userProfileDatabase = TestBed.get(UserProfileDatabase);
    httpClient = TestBed.get(HttpClient);
    httpTestingController = TestBed.get(HttpTestingController);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('form invalid when empty', () => {
    expect(component.signinForm.valid).toBeFalsy();
  });

  it('should run ngOnInit 1 time', fakeAsync(() => {
    fixture.detectChanges();
    spyOn(component, 'ngOnInit').and.callThrough();
    component.ngOnInit();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.ngOnInit).toHaveBeenCalledTimes(1);
  }));

  it('should have a title of Sign In', () => {
    const title: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#home-title'
    );
    expect(title.textContent).toEqual('Sign In');
  });

  it('should run signIn 1 time', fakeAsync(() => {
    fixture.detectChanges();
    spyOn(component, 'signIn').and.callThrough();
    component.signIn();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.signIn).toHaveBeenCalledTimes(1);
  }));

  it('should set the user object to the values set by signIn', fakeAsync(() => {
    const user = {
      ppsn: '13322277P',
      day: '1',
      month: '1',
      year: '1970',
      password: 'Testing123!',
    };

    let ppsn: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#ppsn'
    );
    let day: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#day'
    );
    let month: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#month'
    );
    let year: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#year'
    );
    let password: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#password'
    );

    ppsn.value = '13322277P';
    day.value = '1';
    month.value = '1';
    year.value = '1970';
    password.value = 'Testing123!';

    fixture.detectChanges();
    spyOn(component, 'signIn').and.callThrough();
    component.signIn();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.signIn).toHaveBeenCalledTimes(1);
    expect(component.userDetails).toEqual(user);
  }));

  it('should not match the user object to the values set by signIn', fakeAsync(() => {
    const user = {
      ppsn: '13322277P',
      day: '1',
      month: '2',
      year: '1970',
      password: 'Testing123!',
    };

    let ppsn: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#ppsn'
    );
    let day: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#day'
    );
    let month: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#month'
    );
    let year: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#year'
    );
    let password: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
      '#password'
    );

    ppsn.value = '13322277P';
    day.value = '1';
    month.value = '1';
    year.value = '1970';
    password.value = 'Testing123!';

    fixture.detectChanges();
    spyOn(component, 'signIn').and.callThrough();
    component.signIn();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.signIn).toHaveBeenCalledTimes(1);
    expect(component.userDetails).not.toEqual(user);
  }));
});

AuthService which Injects the Storage

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { throwError, Observable, BehaviorSubject } from 'rxjs';
import { AppConstants } from '../../constants/app.constants';
import { Storage } from '@ionic/storage';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  authenticationState = new BehaviorSubject(false);
  authDetails = {authenticated: false, type: null, myAccountPortalAuthenticated: false };

  constructor(private storage: Storage, private httpClient: HttpClient, private constants: AppConstants) { }

  handleError(error: HttpErrorResponse) {
    let errorMessage = 'Unknown error!';
    if (error.error instanceof ErrorEvent) {
      errorMessage = `Error: ${error.error.message}`;
    } else {
      errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    return throwError(errorMessage);
  }

  login(url): Observable<any> {
    return this.httpClient.post(url, {})
    .pipe(
      catchError((err: HttpErrorResponse) => {
        return throwError(err);
      })
    );
  }

  devLogin(url): Observable<any> {
    return this.httpClient.get(this.constants.LOCAL_REST_API_SERVER + '/users')
    .pipe(
      catchError((err: HttpErrorResponse) => {
        return throwError(err);
      })
    );
  }

  logout(url): Observable<any> {
    return this.httpClient.post(url, {})
    .pipe(
      catchError((err: HttpErrorResponse) => {
        return throwError(err);
      })
    );
  }

  getAuthenticationDetails() {
    return this.storage.get('auth');
  }

  getAuthenticationState(): Observable<boolean> {
    return this.authenticationState.asObservable();
  }

  setIsAuthenticated(value, loginType) {
    this.authDetails.authenticated = value;
    this.authDetails.type = loginType;
    this.storage.set('auth', this.authDetails);
    this.authenticationState.next(value);
  }

}

Module for SignIn

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';
import { UserProfileDatabase } from '../../services/user-profile/user-profile-db.service';
import { UserProfileService } from '../../services/user-profile/user-profile.service';
import { AuthService } from '../../services/auth-service/auth.service';
import { SignInPageRoutingModule } from './sign-in-routing.module';
import { SignInPage } from './sign-in.page';
import { ReactiveFormsModule } from '@angular/forms';
import { Storage, IonicStorageModule } from '@ionic/Storage';


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    SignInPageRoutingModule,
    ReactiveFormsModule,
    IonicStorageModule.forRoot()
  ],
  declarations: [SignInPage],
  providers: [UserProfileDatabase, UserProfileService, AuthService]
})
export class SignInPageModule {}


Solution

  • Change your script to :-

    import {
    async,
    ComponentFixture,
    TestBed,
    fakeAsync,
    tick,
    } from "@angular/core/testing";
    import { IonicModule } from "@ionic/angular";
    import { RouterTestingModule } from "@angular/router/testing";
    import { FormsModule, ReactiveFormsModule } from "@angular/forms";
    import { HttpClient } from "@angular/common/http";
    import {
    HttpClientTestingModule,
    HttpTestingController,
    } from "@angular/common/http/testing";
    import { SignInPage } from "./sign-in.page";
    import { User } from "src/app/classes/user";
    import { AuthService } from "../../services/auth-service/auth.service";
    import { UsersDataPublic } from "../../services/users-service/users-data-public";
    import { UserProfileService } from "../../services/user-profile/user-profile.service";
    import { UserProfileDatabase } from "../../services/user-profile/user-profile-db.service";
    import { AppConstants } from "../../constants/app.constants";
    import {
    NO_ERRORS_SCHEMA,
    CUSTOM_ELEMENTS_SCHEMA,
    inject,
    } from "@angular/core";
    
    class MockAuthService {
    constructor() {}
    handleError(error) {}
    login(url) {}
    devLogin(url) {}
    logout(url) {}
    getAuthenticationDetails() {}
    getAuthenticationState() {}
    setIsAuthenticated() {}
    }
    
    describe("SignInPage", () => {
    let component: SignInPage;
    let httpTestingController: HttpTestingController;
    let fixture: ComponentFixture<SignInPage>;
    let httpClient: HttpClient;
    let service: UsersDataPublic;
    let userProfileservice: UserProfileService;
    let authService: AuthService;
    let auth;
    let userProfileDatabase: UserProfileDatabase;
    let constants: AppConstants;
    
    beforeEach(async(() => {
    TestBed.configureTestingModule({
    declarations: [SignInPage],
    providers: [
    UsersDataPublic,
    UserProfileService,
    UserProfileDatabase,
    AppConstants,
    { provide: AuthService, useClass: MockAuthService },
    ],
    imports: [
    IonicModule.forRoot(),
    ReactiveFormsModule,
    FormsModule,
    RouterTestingModule,
    FormsModule,
    HttpClientTestingModule,
    ],
    schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA],
    }).compileComponents();
    
    fixture = TestBed.createComponent(SignInPage);
    constants = TestBed.get(AppConstants);
    service = TestBed.get(UsersDataPublic);
    userProfileservice = TestBed.get(UserProfileService);
    userProfileDatabase = TestBed.get(UserProfileDatabase);
    httpClient = TestBed.get(HttpClient);
    httpTestingController = TestBed.get(HttpTestingController);
    authService = TestBed.get(AuthService);
    component = fixture.componentInstance;
    fixture.detectChanges();
    }));
    
    it("should create", () => {
    expect(component).toBeTruthy();
    });
    
    it("form invalid when empty", () => {
    expect(component.signinForm.valid).toBeFalsy();
    });
    
    it("should run ngOnInit 1 time", fakeAsync(() => {
    fixture.detectChanges();
    spyOn(component, "ngOnInit").and.callThrough();
    component.ngOnInit();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.ngOnInit).toHaveBeenCalledTimes(1);
    }));
    
    it("should have a title of Sign In", () => {
    const title: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#home-title"
    );
    expect(title.textContent).toEqual("Sign In");
    });
    
    it("should run signIn 1 time", fakeAsync(() => {
    fixture.detectChanges();
    spyOn(component, "signIn").and.callThrough();
    component.signIn();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.signIn).toHaveBeenCalledTimes(1);
    }));
    
    it("should set the user object to the values set by signIn", fakeAsync(() => {
    const user = {
    ppsn: "13322277P",
    day: "1",
    month: "1",
    year: "1970",
    password: "Testing123!",
    };
    
    let ppsn: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#ppsn"
    );
    let day: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#day"
    );
    let month: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#month"
    );
    let year: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#year"
    );
    let password: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#password"
    );
    
    ppsn.value = "13322277P";
    day.value = "1";
    month.value = "1";
    year.value = "1970";
    password.value = "Testing123!";
    
    fixture.detectChanges();
    spyOn(component, "signIn").and.callThrough();
    component.signIn();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.signIn).toHaveBeenCalledTimes(1);
    expect(component.userDetails).toEqual(user);
    }));
    
    it("should not match the user object to the values set by signIn", fakeAsync(() => {
    const user = {
    ppsn: "13322277P",
    day: "1",
    month: "2",
    year: "1970",
    password: "Testing123!",
    };
    
    let ppsn: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#ppsn"
    );
    let day: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#day"
    );
    let month: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#month"
    );
    let year: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#year"
    );
    let password: HTMLInputElement = fixture.debugElement.nativeElement.querySelector(
    "#password"
    );
    
    ppsn.value = "13322277P";
    day.value = "1";
    month.value = "1";
    year.value = "1970";
    password.value = "Testing123!";
    
    fixture.detectChanges();
    spyOn(component, "signIn").and.callThrough();
    component.signIn();
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(component.signIn).toHaveBeenCalledTimes(1);
    expect(component.userDetails).not.toEqual(user);
    }));
    });