Jest mocking module - node.js

I am writing unit tests for a node application using Jest.
The Node code is using a third party library to log information.
The library has a function getLogger which you should call to return a logger object.
I am trying to mock the calls for that library and detect its calls in my unit test.
The node code is as follows:
const logger = require('third-party-libary').getLogger('myModule')
....
function submitSomething() {
....
logger.info('log something')
}
In my Jest unit test, I tried to mock those logger calls in many different ways, with no success, and always come back as "logger is not defined"
I tried:
jest.mock('third-party-library');
const loggerFactory = require('third-party-library');
const logger = {
error: jest.fn(),
info: jest.fn()
};
loggerFactory.getLogger.mockImplementation(() => logger);
But it always return error :
cannot find "info" for null object
I tried this as well:
jest.mock('third-party-library')
const loggerFactory = require('third-party-library');
const logger = {
error: jest.fn(),
info: jest.fn()
};
loggerFactory.getLogger = () => logger
I tried this:
jest.mock('third-party-library')
const loggerFactory = require('third-party-library');
const logger = {
error: jest.fn(),
info: jest.fn()
};
loggerFactory.getLogger = jest.fn(() => logger)
With the same error
I switched between the jest.mock to make it after the require, with no luck

Your approach works fine, just note that your code creates logger as soon as it runs so the mock for getLogger has to be in place before the code is required:
jest.mock('third-party-library');
const loggerFactory = require('third-party-library');
const logger = {
error: jest.fn(),
info: jest.fn()
};
// const { submitSomething } = require('./code'); <= would NOT work here
loggerFactory.getLogger.mockReturnValue(logger);
const { submitSomething } = require('./code'); // <= works here
test('submitSomething', () => {
submitSomething();
expect(logger.info).toHaveBeenCalledWith('log something'); // Success!
});

Related

Mocking a function in a jest environment file

I want to share a server between all my tests, to do this I create the file server-environment.js
const NodeEnvironment = require('jest-environment-node')
const supertest = require('supertest')
//A koa server
const { app, init } = require('../src/server')
class ServerEnvironment extends NodeEnvironment {
constructor(config, context) {
super(config, context)
this.testPath = context.testPath
}
async setup() {
await init
this.server = app.listen()
this.api = supertest(this.server)
this.global.api = this.api
}
async teardown() {
this.server.close()
}
}
module.exports = ServerEnvironment
The thing is that I want to mock some middleware that the servers routes use but I can't really find a way to do that. If I try to declare jest.mock anywhere in the file I get the error that jest isn't defined. If I mock the function in the actual test file the global wouldn't make use of it. Not sure if something like this would be possible to do with Jest?
I had a same issue and solved it by adding setupFilesAfterEnv.
jest.config.js:
module.exports = {
...
setupFilesAfterEnv: [
'./test/setupMocks.js'
]
...
};
test/setupMocks.js
jest.mock('path/to/api', () => global.api);

Jest mock values returned by function

I have a logger file as below which implements logging functionality. uuidLogger.js
const winston = require('winston'),
CustomTransport = require('./customTransport');
function getLogger(route) {
return winston.createLogger({
defaultMeta: { route },
transports: [new CustomTransport()]
});
}
module.exports = getLogger;
It is imported by a function like this and used for logging testfn.js
const uuidLogger = require('./uuidLogger')('test-fn');
function testMock() {
uuidLogger.info('Hey I am just logging');
}
module.exports = { testMock };
I am trying to mock uuidlogger in testfn.js so that I can track various methods called on uuidLogger object. I tried below approach.
import { testMock } from './testfn';
import getLogger from './uuidLogger';
const logger = getLogger('testfn');
jest.mock('./uuidLogger', () =>
jest.fn(() => ({
info: jest.fn(() => console.log('Mocked function actually called'))
}))
);
it('verify that info method was called on returned object', () => {
testMock('abx');
expect(logger.info).toBeCalledTimes(1);
});
It was able to mock the method called however mock information is not getting reflected in logger.info object.
I also tried below approach
import { testMock } from './testfn';
import getLogger from './uuidLogger';
jest.mock('./uuidLogger', () =>
jest.fn(() => ({ info: jest.fn(() => console.log('Top level fn')) }))
);
const logger = {
error: jest.fn(),
info: jest.fn(() => {
console.log('Overwritten fn');
})
};
getLogger.mockReturnValue(logger);
it('shud return Winston instance', () => {
testMock('abx');
expect(logger.info).toBeCalledTimes(1);
});
Any help on how to get it will be appreciated. Thanks in advance.
It seems to be the assertion is not done on proper variable.
Need to assert on getLogger
Your first approach of writing test case is proper.
Add assertion something like this:
expect(getLogger.mock.results[0].value.info).toBeCalledTimes(1);

How to stub Winston logger with sinon

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

Mock fs function with jest

First of all, I'm new to es6 and jest.
I have a Logger class for instantiate winston and I would like to test it.
Here my code :
const winston = require('winston');
const fs = require('fs');
const path = require('path');
const config = require('../config.json');
class Logger {
constructor() {
Logger.createLogDir(Logger.logDir);
this.logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new (winston.transports.Console)({
format: winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.simple(),
),
}),
new (winston.transports.File)({
filename: path.join(Logger.logDir, '/error.log'),
level: 'error',
}),
new (winston.transports.File)({
filename: path.join(Logger.logDir, '/info.log'),
level: 'info',
}),
new (winston.transports.File)({
filename: path.join(Logger.logDir, '/combined.log'),
}),
],
});
}
static get logDir() {
return (config.logDir == null) ? 'log' : config.logDir;
}
static createLogDir(logDir) {
if (!fs.existsSync(logDir)) {
// Create the directory if it does not exist
fs.mkdirSync(logDir);
}
}
}
exports.logger = new Logger().logger;
export default new Logger();
I would like to test my function createLogDir().
I my head, I think it's a good idea to test the state of fs.existsSync.
If fs.existsSync return false, fs.mkdirSync must be called.
So I try to write some jest test :
describe('logDir configuration', () => {
test('default path must be used', () => {
const logger = require('./logger');
jest.mock('fs');
fs.existsSync = jest.fn();
fs.existsSync.mockReturnValue(false);
const mkdirSync = jest.spyOn(logger, 'fs.mkdirSync');
expect(mkdirSync).toHaveBeenCalled();
});
});
However, I've got an error :
● logDir configuration › default path must be used
Cannot spy the fs.mkdirSync property because it is not a function; undefined given instead
18 | fs.existsSync = jest.fn();
19 | fs.existsSync.mockReturnValue(true);
> 20 | const mkdirSync = jest.spyOn(logger, 'fs.mkdirSync');
21 | expect(mkdirSync).toHaveBeenCalled();
22 | });
23 | });
at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:590:15)
at Object.test (src/logger.test.js:20:28)
Can you help me to debug and test my function please ?
Regards.
The error there is because it is looking for a method called fs.mkdirSync on your logger object, which doesn't exist. If you had access to the fs module in your test then you would spy on the mkdirSync method like this:
jest.spyOn(fs, 'mkdirSync');
However, I think you need to take a different approach.
Your createLogDir function is a static method - meaning that it can only be called on the class, and not on an instance of that class (new Logger() is an instance of the class Logger). Therefore, in order to test that function you need to export the class and not an instance of it, i.e.:
module.exports = Logger;
Then you could have the following tests:
const Logger = require('./logger');
const fs = require('fs');
jest.mock('fs') // this auto mocks all methods on fs - so you can treat fs.existsSync and fs.mkdirSync like you would jest.fn()
it('should create a new log directory if one doesn\'t already exist', () => {
// set up existsSync to meet the `if` condition
fs.existsSync.mockReturnValue(false);
// call the function that you want to test
Logger.createLogDir('test-path');
// make your assertion
expect(fs.mkdirSync).toHaveBeenCalled();
});
it('should NOT create a new log directory if one already exists', () => {
// set up existsSync to FAIL the `if` condition
fs.existsSync.mockReturnValue(true);
Logger.createLogDir('test-path');
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
Note: it looks like you're mixing CommonJS and es6 module syntax (export default is es6) - I would try to stick to one or the other

Is there a trick to using Mockery in Mocha test with Typescript?

It would seem the usual method of importing in typescript prevents the modules from being mocked... Assume I have the following product code in a node.js project written in typescript that I would like to test:
// host.ts
import http = require('http');
export class Host {
public start(port: number): http.Server {
return http.createServer().listen(port);
}
}
I have the below unit test using mockery (d.ts in pull request #3313) and mocha:
import chai = require('chai');
import mockery = require('mockery');
import webserver = require('../hosting/host');
describe('host', (): void => {
describe('start()', (): void => {
before(() : void => {
mockery.enable();
});
after((): void => {
mockery.deregisterAll();
mockery.disable();
});
it('should create an http server', (): void => {
mockery.registerMock('http', {
Server: mocks.Server,
createServer: (app: any) : any => new mocks.Server(app)
});
var host: webserver.Host = new webserver.Host({ port: 111 });
var server: any = host.start();
chai.expect(server).is.instanceOf(mocks.Server);
});
});
});
module mocks {
'use strict';
export class Server {
app: any;
constructor(app: any) {
this.app = app;
}
}
}
The problem is that when import webserver = require('../hosting/host') is called the mocks in the test aren't setup yet and the un-mocked require('http') is returned. I attempted to try var http = require('http') within the Host.start function, but this prevents http.Server from being declared as a return value.
How should I go about implementing unit tests in Typescript with Mocks? Is there a better library than mockery that would work better?
After all day of scouring the web I finally learned that: Yes, there is a trick to using Mockery in Mocha test with Typescript. The trick is using the typeof identifier to reference the module. I discovered this in the Optional Module Loading and Other Advanced Loading Scenarios in this document.
My updated code now looks like this:
// host.ts
import httpDef = require('http');
export class Host {
public start(port: number): httpDef .Server {
var http: typeof httpDef = require('http');
return http.createServer().listen(port);
}
}
This allows me to set up mocks in my mocha test like this:
import chai = require('chai');
import mockery = require('mockery');
import webserver = require('../hosting/host');
import httpDef = require('http'):
describe('host', (): void => {
describe('start()', (): void => {
before(() : void => {
mockery.enable();
});
after((): void => {
mockery.deregisterAll();
mockery.disable();
});
it('should create an http server', (): void => {
var mockServer: httpDef.Server = <httpDef.Server>{};
var mockHttp: typeof httpDef = <typeof httpDef>{};
mockHttp.createServer = () : httpDef.Server => mockServer;
mockery.registerMock('http', mockHttp);
var host: webserver.Host = new webserver.Host({ port: 111 });
var server: any = host.start();
chai.expect(server).is.equals(mockServer);
});
});
});
Some other scenarios where this can be used for dependency injection can be found here.

Resources