Pesquisa de site

typescript - Como usar Jest para simular uma instância do logger winston encapsulada em uma classe de serviço


Estou tentando simular uma instância de winston.Logger que está encapsulada dentro de uma classe de serviço criada com NestJS. Incluí meu código abaixo.

Não consigo fazer com que a instância do logger mockado seja acionada de dentro da classe de serviço. Alguém pode explicar onde estou errando?

import * as winston from 'winston';

import { loggerOptions } from '../logger/logger.config';
import { LoggingService } from '../logger/logger.service';

const logger: winston.Logger = winston.createLogger(loggerOptions);

// trying to mock createLogger to return a specific logger instance
const winstonMock = jest.mock('winston', () => (
    {
        format: {
            colorize: jest.fn(),
            combine: jest.fn(),
            label: jest.fn(),
            timestamp: jest.fn(),
            printf: jest.fn()
        },
        createLogger: jest.fn().mockReturnValue(logger),
        transports: {
            Console: jest.fn()
        }
    })
);


describe("-- Logging Service --", () => {
    let loggerMock: winston.Logger;

    test('testing logger log function called...', () => {        
        const mockCreateLogger = jest.spyOn(winston, 'createLogger');
        const loggingService: LoggingService = LoggingService.Instance;
        loggerMock = mockCreateLogger.mock.instances[0];
        expect(loggingService).toBeInstanceOf(LoggingService)
        expect(loggingService).toBeDefined();
        expect(mockCreateLogger).toHaveBeenCalled()

        // spy on the winston.Logger instance within this test and check
        // that it is called - this is working from within the test method
        const logDebugMock = jest.spyOn(loggerMock, 'log');
        loggerMock.log('debug','test log debug');
        expect(logDebugMock).toHaveBeenCalled();

        // now try and invoke the logger instance indirectly through the service class
        // check that loggerMock is called a second time - this fails, only called once
        // from the preceding lines in this test
        loggingService.debug('debug message');
        expect(logDebugMock).toHaveBeenCalledTimes(2);
    });

   ...

Código do método debug do LoggingService

public debug(message: string) {
        this.logger.log(
            {
                level: types.LogLevel.DEBUG,
                message: message,
                meta: {
                    context: this.contextName
                }
            }
        );
    }

Atualização: 09/03/2019

Refatorei meu LoggingService do NestJS para injetar por dependência uma instância do logger do Winston no construtor para facilitar os testes unitários. Isso me permite usar jest.spyOn no método log do logger do Winston e verificar se ele foi chamado dentro da instância do serviço:

// create winstonLoggerInstance here, e.g. in beforeEach()....
const winstonLoggerMock = jest.spyOn(winstonLoggerInstance, 'log');
serviceInstance.debug('debug sent from test');
expect(winstonLoggerMock).toHaveBeenCalled();

Resposta #1

Eu testei seu código e parece que há múltiplos problemas com o uso do jest.mock.

Para simular um módulo corretamente, você deve simulá-lo primeiro, antes de importá-lo. Este é um mecanismo interno (como o jest simula módulos) e você deve seguir esta regra.

const logger = {
  debug: jest.fn(),
  log: jest.fn()
};

// IMPORTANT First mock winston
jest.mock("winston", () => ({
  format: {
    colorize: jest.fn(),
    combine: jest.fn(),
    label: jest.fn(),
    timestamp: jest.fn(),
    printf: jest.fn()
  },
  createLogger: jest.fn().mockReturnValue(logger),
  transports: {
    Console: jest.fn()
  }
}));

// IMPORTANT import the mock after
import * as winston from "winston";
// IMPORTANT import your service (which imports winston as well)
import { LoggingService } from "../logger/logger.service";

Como você pode ver, você não pode usar uma instância do Winston como valor de retorno para seu mock, mas não se preocupe, faça o mock da instância também. (você pode ver isso no exemplo de código anterior também)

const logger = {
  debug: jest.fn(),
  log: jest.fn()
};

Finalmente, você não precisa espionar o que você zombou uma vez, então apenas pergunte diretamente ao mock.

O código completo está aqui:

const logger = {
  debug: jest.fn(),
  log: jest.fn()
};

// trying to mock createLogger to return a specific logger instance
jest.mock("winston", () => ({
  format: {
    colorize: jest.fn(),
    combine: jest.fn(),
    label: jest.fn(),
    timestamp: jest.fn(),
    printf: jest.fn()
  },
  createLogger: jest.fn().mockReturnValue(logger),
  transports: {
    Console: jest.fn()
  }
}));

import * as winston from "winston";
import { LoggingService } from "./logger.service";

describe("-- Logging Service --", () => {
  let loggerMock: winston.Logger;

  test("testing logger log function called...", () => {
    const mockCreateLogger = jest.spyOn(winston, "createLogger");
    const loggingService: LoggingService = LoggingService.Instance;
    loggerMock = mockCreateLogger.mock.instances[0];
    expect(loggingService).toBeInstanceOf(LoggingService);
    expect(loggingService).toBeDefined();
    expect(mockCreateLogger).toHaveBeenCalled();

    // spy on the winston.Logger instance within this test and check
    // that it is called - this is working from within the test method
    logger.log("debug", "test log debug");
    expect(logger.log).toHaveBeenCalled();

    // now try and invoke the logger instance indirectly through the service class
    // check that loggerMock is called a second time - this fails, only called once
    // from the preceding lines in this test
    loggingService.debug("debug message");

    expect(logger.debug).toHaveBeenCalledTimes(1); // <- here
  });
});

Eu mudei a asserção final para um, porque eu chamei log no teste, e debug no LoggingService.

Este é o serviço de registro que eu usei:

import * as winston from "winston";

export class LoggingService {
  logger: winston.Logger;

  static get Instance() {
    return new LoggingService();
  }

  constructor() {
    this.logger = winston.createLogger();
  }

  debug(message: string) {
    this.logger.debug(message);
  }
}

Divirta-se!

Resposta #2

Recentemente, tive a mesma pergunta e resolvi usando jest.spyOn com meu logger personalizado.

NOTA: Você não deveria precisar fazer teste unitário do winston.createLogger(). O módulo Winston possui seus próprios testes unitários que cobrem essa funcionalidade.

Alguma função que registra um erro (ou seja, ./controller.ts):

import defaultLogger from '../config/winston';

export const testFunction = async () => {
  try {
    throw new Error('This error should be logged');
  } catch (err) {
    defaultLogger.error(err);
    return;
  }
};

O arquivo de teste para essa função (ou seja, `./tests/controller.test.ts`):

import { Logger } from 'winston';
import defaultLogger from '../../config/winston';
import testFunction from '../../controller.ts';

const loggerSpy = jest.spyOn(defaultLogger, 'error').mockReturnValue(({} as unknown) as Logger);

test('Logger should have logged', async (done) => {
  await testFunction();

  expect(loggerSpy).toHaveBeenCalledTimes(1);
});

Artigos relacionados