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

Posted

技术标签:

【中文标题】如何使用 Jest 模拟封装在服务类中的 winston 记录器实例【英文标题】:How to use Jest to mock winston logger instance encapsulated in service class 【发布时间】:2020-01-06 08:34:09 【问题描述】:

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

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

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 记录器实例,以方便单元测试。这使我能够在 winston 记录器的 log 方法上使用 jest.spyOn 并检查它是否已在服务实例中被调用:

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

【问题讨论】:

【参考方案1】:

我最近遇到了同样的问题,并通过使用 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);
);

【讨论】:

【参考方案2】:

我已经测试了您的代码,使用 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);
  

玩得开心!

【讨论】:

感谢 Bálint,您的回答非常有帮助。所以我在这里学到的关键教训是首先定义任何模拟并确保它们独立于被模拟的库,例如模拟从库函数返回的任何 API 对象,例如本例中的 winston.createLogger。在定义了模拟之后,然后进行导入。不知道我明白你不需要监视你曾经嘲笑过的东西是什么意思,你的意思是const mockCreateLogger = jest.spyOn(winston, "createLogger");这行吗?再次感谢 Bálint,这非常有帮助 :) 您必须在导入模块之前模拟模块以供实际使用。这个很重要。我敢打赌,Jest 在模拟某些模块时会替换导入的结果。如果您已经有了实际模块的引用(不是模拟),恐怕Jest 无法处理它。您不必监视模拟函数,因为它已经具有间谍的行为。 Jest 可以对间谍或模拟进行相同的断言。 好的,谢谢 Bálint,所以我可以这样做..const winstonMock = 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() // trying to mock createLogger to return a specific logger instance jest.mock("winston", () =&gt; (winstonMock)); 然后测试 createLogger 模拟函数已使用行调用 expect(winstonMock.createLogger).toHaveBeenCalled();而不是使用 spyOn??抱歉重新编码格式!!!! 没错!它更简单、更安全。不要忘记在测试服结束时恢复模拟模块。 知道了。再次感谢您的帮助 Balint,非常感谢 :)

以上是关于如何使用 Jest 模拟封装在服务类中的 winston 记录器实例的主要内容,如果未能解决你的问题,请参考以下文章

如何模拟特定功能而不影响同一个ES6类中的其他功能?

使用 jest typescript 模拟 Axios 的类型

如何使用 jest 模拟链式函数调用?

如何模拟在使用 Jest 测试的 React 组件中进行的 API 调用

使用 Jest/Typescript 测试 fs 库函数

如何使用 jest.fn() 在 jest 中使用 typescript 模拟函数