如何使用Jest模拟封装在服务类中的Winston记录器实例

如何使用Jest模拟封装在服务类中的Winston记录器实例

问题描述:

我正在尝试模拟 winston.Logger 实例,该实例封装在使用NestJS创建的服务类中.我在下面包含了我的代码.

I am trying to mock a winston.Logger instance that is encapsulated within a service class created with NestJS. I have included my code below.

我无法从服务类中触发模拟的记录器实例.谁能解释我要去哪里错了?

I cannot get the mocked logger instance to be triggered from within the service class. Can anyone explain where I am going wrong?

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

   ...

LoggingService调试方法代码

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

更新:2019年3月9日

将我的nestjs LoggingService重构为在构造函数中依赖注入winston logger实例,以方便进行单元测试.这使我可以在Winston logger的log方法上使用 jest.spyOn 并检查是否已在服务实例中调用它:

Refactored my nestjs LoggingService to dependency inject winston logger instance in constructor to facilitate unit testing. This enables me to use jest.spyOn on the winston logger's log method and check that it has been called within the service instance:

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

我已经测试了您的代码,看来jest.mock的使用存在多个问题.

I have tested your code and it seems there are multiple issues with the usage of jest.mock.

为了正确地模拟模块,必须先对其进行模拟,然后再导入.这是一种内部机制(玩笑如何嘲笑模块),您必须遵循此规则.

In order to mock a module properly, you must mock it first, before you import it. This is an internal mechanism (how jest mocks modules) and you must follow this rule.

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";

如您所见,您不能将Winston实例用作模拟的返回值,但也不必担心,也要模拟该实例. (您也可以在前面的代码示例中看到它)

As you can see, you cannot use a winston instance as a returning value for your mock, but no worries, mock the instance as well. (you can see it in the previous code example as well)

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

最后,您无需监视曾经嘲弄过的内容,只需直接询问模拟内容即可.

Finally, you don't need to spy what you have mocked once, so just ask the mock directly.

完整的代码在这里:

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

我将最终断言更改为一个,因为我在测试中调用了log,在LoggingService中调用了debug.

I changed the final assertion to one, because I called log in the test, and debug in the LoggingService.

这是我使用的记录器服务:

This is the logger service I used:

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

玩得开心!