dcs3spp
dcs3spp

Reputation: 583

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

Upvotes: 10

Views: 24927

Answers (3)

Kat Lim Ruiz
Kat Lim Ruiz

Reputation: 2562

On top of the selected answer, I would add that you don't need to mock the whole Winston object. You can mock a certain part like this:

jest.mock("winston", () => {
    const winston = jest.requireActual("winston");
    winston.transports.Console.prototype.log = jest.fn();
    return winston;
});

That way, you only focus on mocking that part, with the other parts intact.

Upvotes: 3

Borduhh
Borduhh

Reputation: 2195

I recently had the same question and solved it by using jest.spyOn with my custom logger.

NOTE: You shouldn't have to unit test winston.createLogger(). The Winston module has its own unit tests that cover that functionality.

Some function that logs an error(i.e. ./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;
  }
};

The test file for that function (i.e. `./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);
});

Upvotes: 9

csakbalint
csakbalint

Reputation: 786

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!

Upvotes: 18

Related Questions