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();
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With