Pesquisa de site

typescript - Angular usando Karma com Jasmine TypeError: this.Service.<foo> não é uma função no Hook de Ciclo de Vida


Karma/Jasmine usando o comando npm run test -- --no-watch --no-progress, lança o seguinte erro:

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)

Oi,

Estou escrevendo um serviço de Logger em Angular 12.0.x. O serviço encaminha tanto para o ngx-logger (por exemplo, para um servidor externo) quanto para uma store (ngxs).

O arquivo karma.conf.js tem como objetivo o diretório raiz da aplicação.

Serviço de Log bastante direto (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)));
  }

  ... ... ...
}

Os métodos do Logger-Service são chamados de dois locais diferentes:

  • Login-Component
  • Componente-Gerenciador-de-Logs

Usando ng serve isso funciona bem. Sem problemas ao clicar. Funciona como esperado.

Ainda assim, executar Karma/Jasmine usando o comando npm run test -- --no-watch --no-progress gera o erro exibido no topo. Removendo as chamadas no Login-Component, nenhum erro é lançado pelo Karma/Jasmine (=> É apenas no Login-Component). Consequentemente, ambos os Components importam o Logger-Service da mesma maneira, mas SOMENTE o LoginComponent lança o erro.


A diferença: O Login-Component usa a função 'onInfo' em um hook do ciclo de vida do Angular. Tanto ngOnInit() quanto ngAfterViewInit() oferecem o mesmo resultado - Funciona ao vivo (ng serve) mas falha no teste Karma/Jasmine.

Eu preciso do hook do ciclo de vida, pois quero rastrear as páginas visitadas (no cenário básico).

Para registro, os dois arquivos afetados:

  • 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();
  });
});

Aqueles arquivos do Log-Manager-Component parecem (quase) iguais. Como dito antes, a diferença é que no Log-Manager-Component a linha ..

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

.. está em uma função acionada por um botão na página HTML.

Alguma sugestão de como consertar isso? :)

Resposta #1

No teste em TestBed.configureTestingModule, a parte providers: [{ provide: LoggerService, useClass: class {} }] significa que um objeto vazio (quer dizer, o {}) deve ser fornecido em vez do LoggerService como um objeto mock. O objeto vazio {} não possui a função onDebug, e é por isso que o erro this.loggerService.onDebug is not a function é lançado quando o teste é executado. (Com ng serve a aplicação funciona, porque o próprio LoggerService está fornecido, e ele possui a implementação para onDebug.)

Segue que, se você quiser ter um objeto mock para o LoggerService em seu teste, as funções necessárias precisam ser mockadas do próprio LoggerService. Por exemplo, se apenas a função onDebug for necessária:

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

...

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

Acima da função onDebug há uma função vazia agora como exemplo, essa função vazia será chamada nos testes em vez da função onDebug do LoggerService. Esse tipo de uso de objeto mock pode não ser necessário se o próprio LoggerService for fornecido no teste e as funções necessárias forem redefinidas com spies do Jasmine.

Resposta #2

Sua especificação provavelmente falha porque você está usando uma classe vazia como mock:

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

Portanto, o erro onDebug não é uma função é lançado.

Solução 1 useValue

Eu sugeriria passar um valor simulado (useValue:) que contenha uma função onDebug:

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

Solução 2 useClass

Você também poderia fornecer uma classe mock simples.

Dentro do seu arquivo de especificação:

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