Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 debug method code

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

Update: 3/09/2019

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();
like image 315
dcs3spp Avatar asked Dec 14 '22 10:12

dcs3spp


1 Answers

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

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.

The complete code is here:

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

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

Have fun!

like image 140
csakbalint Avatar answered Dec 31 '22 09:12

csakbalint