如何使用 Jest 模拟封装在服务中的 winston logger 实例 class

How to use Jest to mock winston logger instance encapsulated in service class

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

我无法从服务 class 中触发模拟记录器实例。谁能解释我哪里出错了?

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 记录器的日志方法上使用 jest.spyOn 并检查它是否已在服务实例中被调用:

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

我已经测试了你的代码,jest.mock 的使用似乎存在多个问题。

为了正确模拟一个模块,您必须先模拟它,然后再导入它。这是一种内部机制(jest 如何模拟模块),您必须遵守此规则。

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 实例用作模拟的返回值,但不用担心,也模拟该实例。 (你也可以在前面的代码示例中看到它)

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

最后,你不需要窥探你模拟过一次的东西,所以直接问模拟就可以了。

完整代码在这里:

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

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

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

玩得开心!

我最近遇到了同样的问题,并通过使用 jest.spyOn 和我的自定义记录器解决了这个问题。

注意:您不必对 winston.createLogger() 进行单元测试。 Winston 模块有自己的单元测试,涵盖了该功能。

一些记录错误的函数(即./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;
  }
};

该函数的测试文件(即`./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);
});