3

I'm using the following code to mock the dependecy with authservice:

login.component.spec

import { LoginComponent } from "./login.component";
import { ComponentFixture, inject, TestBed } from "@angular/core/testing";
import { async } from "q";
import { MatCardModule } from "@angular/material";
import { AuthService } from "../../services/auth/auth.service";
import { Log } from "@angular/core/testing/src/logger";
import { NO_ERRORS_SCHEMA } from "@angular/core";

class MockAuthService extends AuthService {
  isAuthenticated() {
    return "Mocked";
  }
}

describe("LoginComponent", () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;
  let componentService: AuthService;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [LoginComponent],
      providers: [AuthService],
      imports: [MatCardModule]
    });
    TestBed.overrideComponent(LoginComponent, {
      set: { providers: [{ provide: AuthService, useClass: MockAuthService }] }
    });

    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    componentService = fixture.debugElement.injector.get(AuthService);
  }));

  it("Service injected via component should be and instance of MockAuthService", () => {
    expect(componentService instanceof MockAuthService).toBeTruthy();
  });
});

login.component

import {Component, OnInit} from '@angular/core';

import {AuthService} from '../../services/auth/auth.service';
import {Router} from '@angular/router';
import {GithubService} from '../../services/github/github.service';
import {Errorcode} from './errorcode.enum';


@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.sass'],

})
export class LoginComponent implements OnInit {
  public loginError: string | boolean = false;

  constructor(public authService: AuthService, public router: Router, private data: GithubService) {
  }

  public signInWithGithub(): void {
    this.authService.loginwithGithubProvider()
      .then(this.loginError = null)
      .catch(err => {
        if (err === Errorcode.FIREBASE_POPUP_CLOSED) {
        this.loginError = 'The popup has been closed before authentication';
        }
        if (err === Errorcode.FIREBASE_REQUEST_EXESS) {
          this.loginError = 'To many requests to the server';
        }
      }
    );
  }

  public logout(): void {
    this.authService.logout();
  }

  ngOnInit() {
  }
}

But if i take a look at the results i keep getting the following error:

Error: StaticInjectorError(DynamicTestModule)[AuthService -> AngularFireAuth]: StaticInjectorError(Platform: core)[AuthService -> AngularFireAuth]: NullInjectorError: No provider for AngularFireAuth! in http://localhost:9876/_karma_webpack_/vendor.js (line 59376)

Any idea's how i might solve this?

KENdi
  • 7,576
  • 2
  • 16
  • 31

1 Answers1

4

I have reproduced your issue in a Stackblitz. In the Stackblitz currently the tests are all passing, but you will notice I have commented out your declaration of MockAuthService like so:

// class MockAuthService extends AuthService {
//     isAuthenticated() {
//       return "Mocked";
//     }
// }

and replaced it with:

class MockAuthService implements Partial<AuthService> {
    isAuthenticated() {
      return "Mocked";
    }
    loginwithGithubProvider() {
      return new Promise((resolve, reject) => resolve())
    }
    logout() {}
}

The key difference to notice is I replaced extends with implements.

To reproduce your error, simply comment out my new declaration of MockAuthService, and uncomment out your original one. The error you describe above will reappear.

The reason for this is because when you extend a class you get all the objects, properties, methods, etc of the existing class including the constructor. In your case I'm quite sure the constructor for the original AuthService class has injected AngularFireAuth, just as I did in the Stackblitz in the file support.service.ts where I implemented a stub of AuthService. When you extended the class and then injected it into the TestBed when you overrode the component, then tried to create the component, it attempted to execute the constructor in that original class. However, there was no provider given for AngularFireAuth, therefore it threw the error you see. Simply adding AngularFireAuth to the providers array would not have been the right solution because you are trying to test the component, not the service.

When I want to inject a mock class into a test suite, like you have done here, I usually use implements and then Partial<> so that I don't have to implement all the methods and properties of the original class, and so that I only have mocked what I want to, not drag along the original class implementation details.

There is a pretty good discussion on the differences between implements and extends here.

Some further notes:

  • I had to add two other mocked services, for Router and GithubService. Note that I did these as spies rather than service class mocks, just to show a different way to do these. I notice you had imported NO_ERRORS_SCHEMA, probably to avoid having to define these. I don't use that schema myself, even though it seems fairly popular, since I don't want to mask errors - I would rather have them exposed and fix them. :)
  • I also added another spec just to show how you might take this a next step by testing part of the signInWithGithub() method.

I hope this helps.

dmcgrandle
  • 5,934
  • 1
  • 19
  • 38
  • Thanks! i'm sure this will help me! – Kevin van Schaijk Dec 19 '18 at 08:16
  • worked perfectly! One more question since you helped me this far. How can you test if a loginError is set to a certain errormessage after the promise is rejected? – Kevin van Schaijk Dec 19 '18 at 18:20
  • @KevinvanSchaijk - not sure how I missed this comment for so long, my apologies! I have updated the Stackblitz with some examples for how to test for various `loginError` values returned from a rejected promise. I hope it still helps. :) Note: this also exposed an error in how you had set up your `.then()` in the component - it needs to be a function, like this: `.then(() => this.loginError = null)` details in the Stackblitz. – dmcgrandle Dec 30 '18 at 06:02