i have implemented the logger for node js using winston, morgan and winston-daily-rotate-file, express-http-context. So every day a new log file will be written when any of the http request(morgan) or developer defined logger messages to the file. from the node js. Below format how it will be logged
timestamp || Level || filename || traceId || statusCode || logMessage
Everything is working fine, but when i written the test cases using jest, (beginner to jest). I am not able to cover two lines. Here is the entire code and the folder structure.
To every request i am setting a traceId, which i will extract on the customFormat method and after that i returns the customFormat message, these are the two lines i am not able to cover in jest.
//index.js
const app = require('express')();
const cors = require('cors')
const morgan = require('morgan') // HTTP request logger middleware
const logger = require('./lib/logger')(module) //Logger
const uuid = require('uuid')
const httpContext = require('express-http-context')
const config = require('./config').get(process.env.APP_ENV)
// Use any third party middleware that does not need access to the context here
// app.use(some3rdParty.middleware);
app.use(httpContext.middleware);
// all code from here on has access to the same context for each request
// Run the context for each request.
// Assigning a unique identifier to each request
app.use((req, res, next) => {
httpContext.set('traceId', uuid.v1());
next();
});
// using morgan with winston(logger)
app.use(morgan('combined', {
stream: {
write: (message) => logger[config.logLevel](message)
}
}))
app.use(cors());
app.listen(4000, () => {
console.log('Server running on port 4000');
});
// lib/logger/index.js
const appRoot = require('app-root-path');
const {
createLogger,
format,
transports
} = require('winston');
require('winston-daily-rotate-file');
const {
combine,
label,
} = format;
const config = require('../../config').get(process.env.APP_ENV);
const loggerHelper = require('./helpers')
// Custom settings for each transport
const options = {
dailyRotateFile: {
filename: `${appRoot}/logs/TPS-UI-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
prepend: true,
level: config.logLevel,
timestamp: new Date(),
localTime: true,
}
}
// Instantiate a Winston Logger with the settings
const logger = moduleObj => createLogger({
format: combine(
label({
label: loggerHelper.getFileName(moduleObj)
}),
format.timestamp(),
loggerHelper.customFormat()
),
transports: [
new transports.DailyRotateFile(options.dailyRotateFile),
],
exitOnError: false, // do not exit on handled exceptions
});
module.exports = logger
// lib/logger/helpers/index.js
const loggerHelper = require('../../../helper');
const httpContext = require('express-http-context');
const {
format: {
printf
}
} = require('winston')
/**
* @method checkMessageProp
* @param {message} can be object if developer defined, else it will be string
* if its a network request (morgan requests)
* @returns a fixed format how the status code and message should show
*/
const returnLogMessage = (message) => {
const {
statusCode,
logMsg,
maskedData
} = message;
switch (typeof message) {
case 'object':
let statusCodeToBeLogged = statusCode ? statusCode : "Status code not defined",
logMessageToBeLogged = logMsg ? logMsg : "Log message not defined",
return `${statusCodeToBeLogged} || ${logMessageToBeLogged}`
case 'string':
if (message) {
const messageSplit = message.split('"');
let statusCodeToBeLogged = messageSplit[2].trim().split(" ")[0],
logMessageToBeLogged = messageSplit[1]
return `${statusCodeToBeLogged} || ${logMessageToBeLogged}`;
}
return 'Status Code Not Defined || Log Message Not Defined';
default:
return message;
}
};
/**
* @method getFileName
* @param {moduleObj} the module realted object passed from the require of logger file
* @returns the file name where the logger was invoked
*/
const getFileName = (moduleObj) => {
if (Object.keys(moduleObj).length > 0) {
const tempFileNameParts = moduleObj.filename.split("/");
const fileName = tempFileNameParts.slice(Math.max(tempFileNameParts.length - 2, 1)).join('/');
return fileName;
}
return 'Module not passed while requiring the logger';
};
/**
* @method customFormat
* @param {log} the log passed by the developer or based on network requests
* @returns a customFormat how it should be logged to the log files
*/
const customFormat = () => {
return printf((log) => {
const traceId = httpContext.get('traceId');
return `${new Date(log.timestamp)} || ${log.level.toUpperCase()} || ${log.label} || ${traceId} || ${returnLogMessage(log.message)} `;
})
}
module.exports = {
returnLogMessage,
getFileName,
customFormat
}
// lib/logger/__test__/logger.test.js
jest.mock('winston');
const logger = require('..');
const winston = require('winston');
describe('Given the logger method is called', () => {
let loggerObject;
const mockModuleObject = {
filename: 'server/index.js'
};
beforeEach(() => {
loggerObject = logger(mockModuleObject);
});
test('it should return a object returned by createLogger', () => {
expect(loggerObject).toEqual(winston.mockLoggerObject);
});
test('it should call combine, format, printf and timestamp method of winston', () => {
expect(winston.mockPrintf).toHaveBeenCalled();
expect(winston.mockLabel).toHaveBeenCalled();
expect(winston.mockCombine).toHaveBeenCalled();
});
test('expect Dailytransports to be called', () => {
// how to check the daily transport has been called
expect(winston.mockDailyTransport).toHaveBeenCalled();
});
});
// lib/logger/__test__/helpers/helper.test.js
jest.mock('winston')
jest.mock('express-http-context')
const helper = require('../../helpers/')
const httpContext = require('express-http-context')
const {
format: {
printf
}
} = jest.requireActual('winston')
describe('Given the helper methods for logger should call a d take different scenarios', () => {
let mockMessageObj, mockMessageString, mockMessageStringEmpty, mockMessageNumber;
beforeAll(() => {
mockMessageObj = {
statusCode: 200,
logMsg: "Testing log"
}
mockMessageString = `::1 - - [31/Jan/2019:11:26:54 +0000]
"GET /graphql HTTP/1.1" 404 146 "-" "Mozilla/5.0 (X11; Linux x86_64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"`
mockMessageStringEmpty = ""
mockMessageNumber = 12345
})
test('returnLogMessage and getFileName methods should exist', () => {
expect(helper.returnLogMessage).toBeDefined()
expect(helper.getFileName).toBeDefined()
})
// returnLogMessage Method Test Cases
test('should return a string when passes object', () => {
expect(helper.returnLogMessage(mockMessageObj)).toEqual('200 || Testing log || userId : tne:1***:***2354')
})
test('should return a string when passes string', () => {
expect(helper.returnLogMessage(mockMessageString)).toEqual('404 || GET /graphql HTTP/1.1')
})
test('should return default string when passes string as undefined', () => {
expect(helper.returnLogMessage(mockMessageStringEmpty)).toEqual('Status Code Not Defined || Log Message Not Defined || Mask Data Not Defined')
})
test('should return the actual default message if the type is nor object neither string', () => {
expect(helper.returnLogMessage(mockMessageNumber)).toEqual(12345)
})
test('should return default message for status code', () => {
let tempMockMessageObjStatusCode = { ...mockMessageObj
}
tempMockMessageObjStatusCode["statusCode"] = ""
expect(helper.returnLogMessage(tempMockMessageObjStatusCode)).toEqual('Status code not defined || Testing log || userId : tne:1***:***2354')
})
test('should return default message for log msg', () => {
let tempMockMessageObjLogMsg = { ...mockMessageObj
}
tempMockMessageObjLogMsg["logMsg"] = ""
expect(helper.returnLogMessage(tempMockMessageObjLogMsg)).toEqual('200 || Log message not defined || userId : tne:1***:***2354')
})
test('should return default message for masked data for undefined', () => {
let tempMockMessageObjMaskData = { ...mockMessageObj
}
tempMockMessageObjMaskData["maskedData"] = ""
expect(helper.returnLogMessage(tempMockMessageObjMaskData)).toEqual('200 || Testing log || Masked data not defined')
})
test('should return default message for masked data for empty object', () => {
let tempMockMessageObjMaskData = { ...mockMessageObj
}
tempMockMessageObjMaskData["maskedData"] = {}
expect(helper.returnLogMessage(tempMockMessageObjMaskData)).toEqual('200 || Testing log || Masked data not defined')
})
// getFileName Method Test Cases
test('should return default label when module is not passed', () => {
expect(helper.getFileName({})).toEqual('Module not passed while requiring the logger')
})
})
// this one how can i test the custom format method
describe('custom format', () => {
test('should call the printF function inside customFormat function', () => {
})
})
// __mocks/winston.js
const winston = jest.genMockFromModule('winston');
const mockLoggerObject = {
error: jest.fn(),
info: jest.fn(),
};
const mockLabel = jest.fn();
const mocktimestamp = jest.fn();
const mockPrintf = jest.fn();
const mockCombine = jest.fn();
const mockDailyTransport = jest.fn();
const mockTransports = {
DailyRotateFile: mockDailyTransport
};
const mockCreateLogger = jest.fn().mockReturnValue(mockLoggerObject);
const mockFormat = {
label: mockLabel,
timestamp: mocktimestamp,
printf: mockPrintf,
combine: mockCombine,
};
winston.createLogger = mockCreateLogger;
winston.transports = mockTransports;
winston.mockLoggerObject = mockLoggerObject;
winston.format = mockFormat;
winston.mockLabel = mockLabel;
winston.mocktimestamp = mocktimestamp;
winston.mockPrintf = mockPrintf;
winston.mockDailyTransport = mockDailyTransport;
winston.mockCombine = mockCombine;
module.exports = winston;
//__mocks/express-http-context
const httpContext = jest.genMockFromModule('express-http-context');
const mockGet = jest.fn();
httpContext.get = mockGet;
module.exports = httpContext;
the two test cases i am not able to check is or throwing error is one
// helper.test.js
describe('custom format', () => {
test('should call the printF function inside customFormat function', () => {
// how can i coverage the line what should be written here
})
})
// logger.test.js
test('expect Dailytransports to be called', () => {
expect(winston.mockDailyTransport).toHaveBeenCalled();
});