I am trying to run a test using the winston logger package. I want to spy on the createlogger function and assert that it is being called with the correct argument.
Logger.test.ts
import { describe, expect, it, jest, beforeEach, afterEach } from '#jest/globals';
import { LogLevel } from 'api-specifications';
import winston, { format } from 'winston';
import { buildLogger } from './Logger';
import { LoggerConfig } from './Config';
describe('Logger', () => {
beforeEach(() => {
jest.spyOn(winston, 'createLogger');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should call winston createLogger with format.json when config.json is true', () => {
const config: LoggerConfig = {
json: true,
logLevel: LogLevel.INFO,
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: LogLevel.INFO,
format: format.json(),
}),
);
});
});
Logger.ts
import { createLogger, format, transports, Logger } from 'winston';
import { LoggerConfig } from './Config';
const logFormatter = format(info => {
const values = (info[Symbol.for('splat') as any as string] ?? [])
.filter(f => typeof f === 'object')
.reduce(
(acc, curr) => ({
...acc,
...curr,
}),
{},
);
const meta = Object.keys(values)
.map(k => ` - ${k}=${values[k]}`)
.join('');
return { ...info, [Symbol.for('message')]: `${info.level}: ${info.message}${meta}` };
});
export const buildLogger = (config: LoggerConfig): Logger =>
createLogger({
level: config.logLevel,
format: config.json ? format.json() : logFormatter(),
transports: [new transports.Console()],
});
However when i run the test i get the following output
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: ObjectContaining {"format": {"options": {}}, "level": "info"}
Number of calls: 0
Im not quite sure what going on.
Im using the following versions of packages:
"jest": "28.1.0"
"ts-jest": "28.0.2"
Assuming you are using ES modules, there are many ways to solve this issue. Honestly, I do not know which one is better (all of them have upsides and downsides), there might even be a well-known solution that I have not found yet, but I doubt it. The reason is that, from what I have read, Jest support for ES modules is still incomplete, as the documentation points out:
Please note that we currently don't support jest.mock in a clean way in ESM, but that is something we intend to add proper support for in the future. Follow this issue for updates.
So, all the followings are just workarounds, not real solutions.
#1 - Always import the default object
You can import winston in 2 ways:
import * as winston from 'winston': this notation returns a Module object, containing the exported properties. Among them you can find a default property, pointing to module.exports of the CommonJS module.
import winston from 'winston': this is a syntactic sugar for import { default as winston } from 'winston'. Basically, instead of importing the entire module, you just get the default property.
You can read more about it here.
createLogger can be accessed in 2 ways if you use the first import notation:
[Module] object
{
...
createLogger: f() { ... }
default: {
...
createLogger: f() { ... }
}
}
I am not sure mocking a Module object is possible, but in your case it is enough to mock default.createLogger. This is quite easy:
Logger.ts
import winston from 'winston'
export const buildLogger = async (config) => {
return winston.createLogger({
level: "info"
});
}
(Logger.test.ts is the original one.)
Why does this work? Because both Logger.test.ts and Logger.ts assign to winston (a reference to) the default object. jest.spyOn(winston, 'createLogger') creates a spy on the method default.createLogger, because we have imported only the default object. Therefore, the mocked implementation gets shared with Logger.ts as well.
The downside is that an import statement like import { createLogger } from 'winston' cannot work because you are accessing Module.createLogger instead of Module.default.createLogger.
#2 - First mock, then import
With ES modules, import statements are hoisted: even if the first line of your Logger.test.ts was jest.mock('winston', ...), the Logger module would be loaded before that line (because of import { buildLogger } from './Logger';). This means that, in the current state, Logger.ts references the actual implementation of createLogger:
Jest loads Logger.test.ts
Node module loader loads all the modules imported with import ... from ...
Logger.ts is executed, preceded by with import { createLogger } from 'winston'.
Node continues to execute Logger.test.ts, Jest creates a spy on createLogger, but Logger.ts already references the actual implementation of that method.
To avoid the hoisting, a possibility is to use dynamic imports:
Logger.test.ts
import { jest } from '#jest/globals';
jest.mock('winston', () => {
return {
// __esModule: true,
// default: () => "test",
createLogger: jest.fn()
}
});
const winston = await import('winston')
const { buildLogger } = await import('./Logger');
describe('Logger', () => {
it('should call winston createLogger with format.json when config.json is true', () => {
const config = {
json: true,
logLevel: "info",
};
buildLogger(config);
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: "info"
}),
);
});
});
(Logger.ts is the original one.)
Now the module is mocked before importing the logger dependency, which will see the mocked version of winston. Just a few notes here:
__esModule: true is probably not necessary in your case (or maybe I was not able to correctly mock the ES module without dynamic imports), but in case you have to mock an ES module that you will use in the current test file, then you have to use it. (See here)
I had to configure Jest with transform: {}, see here
The upside is that the implementation code stays unchanged, but the test code becomes more complex to handle and maintain. Besides, there could be some situations where this does not work at all.
#3, #4... 🤔
There is at least another solution out there, but, just looking at the method name, I would not use it: I am talking about unstable_mockModule. I have not found official documentation for it, but it is probably not ready for production code.
Manual mocks could be another way to solve this, but I have not tried it.
Honestly, I am not fully satisfied with any of these solutions. In this case, I would probably use the first one, at the expense of the implementation code, but I really hope someone finds something better.
Try mocking:
jest.mock('winston', () => {
return {
createLogger: jest.fn()
}
});
describe('Logger', () => {
...
Related
I have installed a node module called #SomeOrg/SomeLibrary. It is being imported in source code in multiple ways in multiple files. For example,
import { SOME_IMPORTS_FROM_MODULE1} from "#SomeOrg/SomeLibrary/Module1"
import {SOME_IMPORTS_FROM_SUB_MODULE} from "#SomeOrg/SomeLibrary/Module1/SubModule"
import {SOME_IMPORTS_FROM_MODULE2} from "#SomeOrg/SomeLibrary/Module2"
etc.
How can I mock the #SomeOrg/SomeLibrary using JEST so that it would work correctly on each file's test cases? Actually, I have tried the following way to mock it in a file and added it to the setupFiles key in the JEST config.
jest.mock("#SomeOrg/SomeLibrary", () => ({
Module1: {
SOME_IMPORTS_FROM_MODULE1: jest.fn(),
SubModule: {
SOME_IMPORTS_FROM_SUB_MODULE: jest.fn()
}
},
Module2: {
SOME_IMPORTS_FROM_MODULE2: jest.fn()
}
))
But it is not working. Please suggest me a generic way so that it would work for all relative paths imported from a node module.
Here is the working solution for the above scenario. But this is not an optimal one. Edits are welcome.
1. module1_mock.js
jest.mock('#SomeOrg/SomeLibrary/Module1', () => {
return {
SOME_IMPORTS_FROM_MODULE1: jest.fn()
}
})
2. sub_module_mock.js
jest.mock('#SomeOrg/SomeLibrary/Module1/SubModule', () => {
return {
SOME_IMPORTS_FROM_SUB_MODULE: jest.fn()
}
})
3. module2_mock.js
jest.mock('#SomeOrg/SomeLibrary/Module2', () => {
return {
SOME_IMPORTS_FROM_MODULE2: jest.fn()
}
})
4. module_mock.js
jest.mock('#SomeOrg/SomeLibrary', () => {
return {
__esModule: true,
default: jest.fn(),
}
})
Once the above files are created, add them to the setupFiles key inside the jest configuration file like below.
{
...defaultConfig,
setupFiles: ["DIRECTORY_OF_MOCKS/module1_mock.js","DIRECTORY_OF_MOCKS/sub_module_mock.js","DIRECTORY_OF_MOCKS/module2_mock.js","DIRECTORY_OF_MOCKS/module_mock.js"]
}
I'm new in nodejs, I'm using fastify and I want to be able to use the req.logger in all the classes functions of the flow, this because I have a the request-id on req.logger, the first solution that came to my mind is to pass as a parameter the logger through all the function/classes but I think that would make the code kind of dirty, here is an example of my code:
app.ts
import pino from 'pino';
import fastify from 'fastify';
declare module 'fastify' {
interface FastifyInstance {
// augment fastify instance with the config object types
config: Config;
}
}
function build() {
const app = fastify({
logger: pino({
name: process.env.NAME,
level: process.env.LOG_LEVEL,
}),
disableRequestLogging: true,
requestIdHeader: 'correlation-id',
requestIdLogLabel: 'correlationId',
});
// register plugins
app.register(apiRoutes, fastify => ({
getObjectUseCase: new GetObjectUseCase(
new TestClass()),
}));
return app;
}
export { build };
routes.ts
import { FastifyPluginCallback } from 'fastify';
import { StatusCodes } from 'http-status-codes';
export const apiRoutes: FastifyPluginCallback<RoutesOpts> = async (fastify, options, done) => {
const getObjectUseCase = options.getObjectUseCase;
fastify.get<object>('/v1/api/:id', async (req, reply) => {
const id = req.params.payoutId;
req.logger.info('This is a logger print'); // has the correlation id inside it while printing
const storedObject = await getObjectCase.execute(id);
reply.code(StatusCodes.OK).send(storedObject);
});
}
GetObjectUseCase.ts
export class GetObjectUseCase {
private anotherClass: TestClass;
constructor(anotherClass: TestClass) {
this. anotherClass = anotherClass;
}
async execute(id: string): Promise<StoredObject> {
// I want to use the logger here with have the correlation id on it without having to pass it as an argument on the method, how is it posible?
return this.anotherClass.getById(id);
// also needed to use it inside anotherClass.getById so I will need to pass the logger also in the method
}
}
Hope I have been clear.
Thanks!
This may not be the best or only way to do it, but this has worked for me in the past.
Typically I structure my projects with an app.ts that just instantiates my FastifyInstance and then exports the log from that created instance. This allows me to use the log where ever I want to.
It looks something like this.
app.ts
import fastify from 'fastify';
const app = fastify({ logger: true /* Your logging configuration */});
export default app;
export const logger = app.log; // Allows me to log where ever I want.
server.ts
import app from './app';
... // All your fastify configuration and other stuff.
app.listen({ ... });
Now I can use the logger outside of fastify stuff.
get-object-use-case.ts
import { logger } from './app'; // Import your fastify logger to use in this class.
export class GetObjectUseCase {
private anotherClass: TestClass;
constructor(anotherClass: TestClass) {
this. anotherClass = anotherClass;
}
async execute(id: string): Promise<StoredObject> {
logger.info({/* Whatever you want to log here. */}); // Now you can use the logger here.
return this.anotherClass.getById(id); // You can just import the logger into the TestClass file to get logging enabled there.
}
}
This even allows you to log before your FastifyInstance is started. Check out this codesandbox for a running example.
I want to setup morgan-boddy as a midldleware to log requests and responses.
So I've created a function like that:
export function RequestLogging(app) {
const logger = new Logger('Request');
app.use(
morganBody(app, {
stream: { // <--- error is here "Void function return value is used "
write: (message) => logger.log(message.replace('\n', '')),
},
}),
);
}
That I call on main.ts
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
useRequestLogging(app);
// ...
}
However it seems does not work. I've got an error 'Void function return value is used' on line stream: {
Any idea how to fix?
UPDATE:
I tried to go different path and actually just stick morganBody in to main.ts as per docs:
import bodyParser from 'body-parser';
import morganBody from 'morgan-body';
app.use(bodyParser.json());
// hook morganBody to express app
morganBody(app); <-- getting error here "TS2345: Argument of type 'INestApplication' is not assignable to parameter of type 'Application'."
I wish there was a proper documentation how to approach in nestjs.
This is a pretty interesting middleware. It ends up needing the express instance itself because it calls app.use(morgan) and app.response.send() under the hood. I would really look into some other solution instead of something that accesses the response in this way.
Either way: this set up works
import { Logger } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import * as morgan from 'morgan-body';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = app.get(Logger);
(morgan as any)(app.getHttpAdapter().getInstance(), {
stream: {
write: (message: string) => {
logger.log(message.replace('\n', ''));
return true;
},
},
});
await app.listen(3033);
}
bootstrap();
The types for the package are wrong as well, it's not a default export, but a named one, so import morgan from 'morgan-body' doesn't work as advertised (at least it didn't for me).
The return true is necessary because write expects a stream.writable() method, which has returns a boolean. You can just default this to true. Then you have to use app.getHttpAdapter().getInstance() so that you ensure you pass the express instance to the middleware. Again, wonky setup, but it works.
I have a logger which utilizes the Winston NodeJs package. The logger does additional logic, and I would like to have unit tests to ensure that the proper data is being passed to Winston. However, since I have set up external transports (such as Firehose) I do not need those to be invoked.
I am not passing in Winston as a dependency through the constructor, but I have tried stubbing the createLogger method, the log method, and Winston as a whole as I normally would when stubbing a dependency.
The createStubbedInstance method does not work with Winston (or, I have not been able to get it to work) due to the fact that Winston is not exported as a class, but as a namespace.
import { Logger, ILoggerConfig } from './src';
import * as winston from 'winston'
describe('Logger', () => {
let loggerConfig: ILoggerConfig;
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
loggerConfig = {
correlationId: faker.random.uuid(),
action: 'GET',
};
sandbox = sinon.createSandbox();
winstonStub = sandbox.stub(winston);
winstonStub.createLogger.resolves();
winstonStub.log.resolves();
...
});
it('should log with INFO log level', () => {
const logger = new Logger(loggerConfig);
logger.info('Hello there!');
sinon.assert.calledOnce(winstonStub.log);
sinon.assert.calledWith(winsonStub.log, sinon.match.has("level", 'info'))
});
import { Logger, ILoggerConfig } from './src';
import * as winston from 'winston'
describe('Logger', () => {
let loggerConfig: ILoggerConfig;
let sandbox: sinon.SinonSandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
loggerConfig = {
correlationId: faker.random.uuid(),
action: 'GET',
};
sandbox = sinon.createSandbox();
winstonStub = sandbox.stub(winston, 'createLogger').resolves({ log: sanbox.stub() });
...
});
it('should log with INFO log level', () => {
const logger = new Logger(loggerConfig);
logger.info('Hello there!');
sinon.assert.calledOnce(winstonStub);
});
I would expect to be able to assert that the stub would be called a certain number of times. However, the stub always has a call count of 0, and I get an error indicating that Winston cannot post to Firehose due to permission issues. I also have the Console transport set up, and still see logs in the console when I should not.
I couldn't use "esModuleInterop": true with import winston from 'winston' due to strict settings of the project causing a ripple effect of compilation issues.
Luckily I found a clean way to mock what I needed when mocking log directly failed.
import * as tape from "tape";
import * as sinon from "sinon";
import * as winston from "winston";
tape('Stubbing winston', (test) => {
test.test('Or at least part of it', (test) => {
const logSpy = sinon.spy()
sinon
.stub(winston, "createLogger")
.callsFake(() => ({
log: logSpy,
} as unknown as Logger));
const logger = new MyLoggerThatUsesWinston()
logger.log('Only lost');
logger.log('four hours');
logger.log('on this');
test.equal(logSpy.callCount, 3, "")
test.end();
}
test.end();
}
In my nodejs server, I would like to mock fs for my test with Mocha.
I end up using Mockery, but I really misunderstand a concept.
In my test (I use Typescript as well):
// mock for fs
var fsMock = {
readdir: (path: string) => { return { err: undefined, files: [] } },
writeFile: (path: string, content: string, encoding: string) => { return { err: undefined } },
readFileSync: (path: string, encoding: string) => { return "lol" }
};
Mockery.registerMock('fs', fsMock);
beforeEach((done) => {
Mockery.enable({
useCleanCache: true,
warnOnReplace: false,
warnOnUnregistered: false
});
}
afterEach(() => {
Mockery.disable();
});
but unfortunately during my test, my module still use the old fs. I understood why is not working. Indeed, on my test, I:
import my module at the top of the file
as I import the module, my module will import its dependencies, as fs.
as at this time mockery is not yet enabled (we are still not running a test...), the fs imported in my module is the original one
I setup mockery before my test
I execute my test, that fail because is still the original fs that is used...
Now the question is : how I can tell my module to re-require its dependencies to use my mocked version of fs ? And more globally, how I can mock fs easily ?
Thanks.
Finally, after testing and testing, I have ended up not using mockery, but SinonJS instead. It provides a very easy and effortless way to mock fs, e.g. :
import * as Fs from "fs"
import * as Sinon from "sinon"
// ..
// At a place inside a test where the mock is needed
Sinon.stub(Fs, "readdir").callsFake(
(path: string, callback: Function) => { callback(null, ['toto']) }
);