Angular using Karma with Jasmine TypeError: this.Service.<foo> is not a function in Lifecycle Hook

Viewed 947

Karma/Jasmine using the npm run test -- --no-watch --no-progress-command, throws the following error:

Chrome 92.0.4515.159 (Mac OS 10.15.7) LoginComponent should create FAILED
    TypeError: this.loggerService.onDebug is not a function
        at LoginComponent.ngAfterViewInit (src/app/pages/login/login.component.ts:22:24)
        at callHook (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:2526:1)
        at callHooks (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:2495:1)
        at executeInitAndCheckHooks (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:2446:1)
        at refreshView (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:9516:1)
        at renderComponentOrTemplate (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:9559:1)
        at tickRootContext (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:10790:1)
        at detectChangesInRootView (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:10815:1)
        at RootViewRef.detectChanges (node_modules/@angular/core/__ivy_ngcc__/fesm2015/core.js:22865:1)
        at ComponentFixture._tick (node_modules/@angular/core/__ivy_ngcc__/fesm2015/testing.js:141:1)

Hi there,

I am writing a Logger-Service in Angular 12.0.x. The service pipes to both the ngx-logger (e.g. to an external server) as well as into a store (ngxs).

The karma.conf.js-File aims at root of the application.

Pretty much straight forwad Logger-Service (logging-service.ts):

import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { NGXLogger } from 'ngx-logger';
import { AddLog, LogEntry } from '../store/log-state';

@Injectable({
  providedIn: 'root',
})
export class LoggerService {
  // This service pipes Log-Messages both to the store as well as to the NGXLogger.
  // Further forwarding by the NGXLogger is dependent on the properties provided in the environment.ts-file.

  constructor(private logger: NGXLogger, private store: Store) {}

  private createLogEntry(level: string, origin: string, msg: string) {
    const dateTime = new Date();
    const log: LogEntry = {
      time: dateTime,
      level: level,
      origin: origin,
      msg: msg,
      noticed: false,
    };
    return log;
  }

  onTrace(origin: string, msg: string) {
    // To NGXLogger
    this.logger.trace(msg);
    // To Store
    this.store.dispatch(new AddLog(this.createLogEntry('trace', origin, msg)));
  }

  onDebug(origin: string, msg: string) {
    // To NGXLogger
    this.logger.trace(msg);
    // To Store
    this.store.dispatch(new AddLog(this.createLogEntry('debug', origin, msg)));
  }

  onInfo(origin: string, msg: string) {
    // To NGXLogger
    this.logger.trace(msg);
    // To Store
    this.store.dispatch(new AddLog(this.createLogEntry('info', origin, msg)));
  }

  ... ... ...
}

The Logger-Service's methods are called from two different locations:

  • Login-Component
  • Log-Manager-Component

Using ng serve this works fine. No problems klicking through. Works as expected.

Still, running Karma/Jasmine using the npm run test -- --no-watch --no-progress-command, throws the error displayed on top. Removing the calls in Login-Component, there is no error thrown by Karma/Jasmine (=> It's only in the Login-Component). Consequently both Components import the Logger-Service the same way, but ONLY the LoginComponent throws the error.


The difference: The Login-Component uses the 'onInfo'-Function in an Angular Lifecycle hook. Both ngOnInit() & ngAfterViewInit() offer the same result - Works live (ng serve) but fails the Karma/Jasmine Test.

I need the Lifecycle hook as I want to track the pages visited (in the basic scenario).

For the record the two affected files:

  • login.component.ts
import { Component, AfterViewInit } from '@angular/core';
import { LoggerService } from 'src/app/services/logging.service';

@Component({
  selector: 'lep-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.less'],
})
export class LoginComponent implements AfterViewInit {
  constructor(
    private loggerService: LoggerService,
  ) {}

  ngAfterViewInit() {
    this.loggerService.onDebug('Login', 'SampleMessage');
  }
}
  • login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoggerService } from 'src/app/services/logging.service';

import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [LoginComponent],
      providers: [{ provide: LoggerService, useClass: class {} }],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

Those files from the Log-Manager-Component look (almost) the same. As said before, the difference is that in the Log-Manager-Component the line ..

this.loggerService.onDebug('Login', 'SampleMessage');

.. is in a function triggered by a button on the html-page.

Any suggestions how to fix this? :)

2 Answers

In the test in TestBed.configureTestingModule the providers: [{ provide: LoggerService, useClass: class {} }] part means, that an empty object (I mean the {}) should be provided instead of the LoggerService as a mock object. The {} empty object does not have an onDebug function, this is why the this.loggerService.onDebug is not a function error is thrown when the test is executed. (With ng serve the application works, because there is the LoggerService itself provided which has the implementation for onDebug.)

It follows, if you want to have a mock object for the LoggerService in your test, the needed functions have to be mocked from the LoggerService itself. For example if only the onDebug function is needed:

await TestBed.configureTestingModule({
  declarations: [LoginComponent],
  providers: [{ provide: LoggerService, useClass: LoggerServiceMock }],
}).compileComponents();

...

// Outside of the jasmin describes
class LoggerServiceMock {
  onDebug(origin: string, msg: string) {};
}

Above the onDebug funtion is an empty function now as an example, this empty function will be called in the tests instead of the onDebug function of the LoggerService. This kind of mock object usage might not be necessary if the LoggerService itself is provided in the test and the needed functions are overdefined with jamsine spys.

Your spec probably fails because you´r using an empty class as a mock:

providers: [{ provide: LoggerService, useClass: class {} }]

Thus onDebug is not a function error is thrown.

Solution 1 useValue

I would suggest to pass an mocked value (useValue:) which contains an onDebug function:

providers: [{ provide: LoggerService, useValue: { onDebug: (origin: string, msg: string) => undefined} }]

Solution 2 useClass

You could also provide a simple mock class.

Inside your specfile:

class LoggerServiceMock {
    onDebug(origin: string, msg: string) { // we do nothing here }
}
...
...
providers: [{ provide: LoggerService, useClass: LoggerServiceMock }]
Related