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']) }
);
Related
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', () => {
...
In a Firebase Cloud Function project...
I have the following typescript file at the root of my src directory right along side of my main index.ts file which imports one dependency and exports a class that includes 2 methods. This file is titled bcrypt.class.ts:
import * as bcrypt from 'bcryptjs';
export default class BcryptTool {
public static hashValue(value: string, rounds: number, callback: (error: Error, hash: string) => void) : void {
bcrypt.hash(value, rounds, (error:any, hash:any) => {
callback(error, hash);
});
}
public static compare(value: string, dbHash: string, callback: (error: string | null, match: boolean | null) => void) {
bcrypt.compare(value, dbHash, (err: Error, match: boolean) => {
if(match) {
callback(null, true);
} else {
callback('Invalid value match', null);
}
});
}
}
In my Firebase Cloud functions index.ts file I import this class and make a call to it's 'compare' method within one of my functions without issue, this works as expected:
'use strict';
const express = require('express');
const functions = require('firebase-functions');
const cors = require('cors')({ origin: true });
const admin = require('firebase-admin');
admin.initializeApp();
const api = express();
import BcryptTool from './bcrypt.class'; // <-- i import the class here
// and use it in a function
api.use(cors);
api.post('/credentials', async (request: any, response: any) => {
BcryptTool.compare(...) // <--- calls to this method succeed without issue
});
The problem
My application includes many functions, but I only need the class noted above in one of them, so in an attempt to optimize cold start time for all my other functions, I attempt to dynamically import this class inside of the function that needs it instead of importing it into the global scope as outlined above. This does not work and I cannot figure out why:
'use strict';
const express = require('express');
const functions = require('firebase-functions');
const cors = require('cors')({ origin: true });
const admin = require('firebase-admin');
admin.initializeApp();
const api = express();
api.use(cors);
api.post('/credentials', async (request: any, response: any) => {
const BcryptTool = await import('./bcrypt.class'); // <-- when i attempt to import here instead
BcryptTool.compare(...) // <--- subsequent calls to this method fail
// Additionally, VS Code hinting displays a warning: Property 'compare' does not exist on type 'typeof import('FULL/PATH/TO/MY/bcrypt.class')'
});
Is my class not written or exported correctly?
Am I not importing the class correctly inside of my cloud function?
The top-level import (import BcryptTool from './bcrypt.class';) will automatically import the default export from the bcrypt.class module. However, when using the import statement as a function (so called "dynamic import"), it will import the module itself, not the default export.
You can see the difference when you would console.log(BcryptTool) both imports:
import BcryptTool from './bcrypt.class' will show { default: { [Function: BcryptTool] hashValue: [Function], compare: [Function] } }
const BcryptTool = await require('bcrypt.class') will show { [Function: BcryptTool] hashValue: [Function], compare: [Function] }
Did you notice the default in the first console.log? That shows you imported the module, not the default.
Now actually the import BcryptTool from './bcrypt.class' syntax is syntactic sugar for doing import { default as BcryptTool } from './bcrypt.class'. If you apply this knowledge on the dynamic import, you could do this:
const BcryptToolModule = await import('./bcrypt.class');
BcryptToolModule.default.compare(...);
Or in a cleaner syntax:
const { default: BcryptTool } = await import('./bcrypt.class');
BcryptTool.compare(...);
This question already has an answer here:
What is the correct way to dynamically import a class inside of a Firebase Cloud function using typescript?
(1 answer)
Closed 3 years ago.
If I have a module that is only required for one of my Firebase Cloud functions, this Firebase Tutorial suggests importing that module inside just the function that needs it, in order to minimize cold start time for all other functions in a project.
This makes sense, but is it also possible to import a class which contains its own set of dependencies inside of a function?
I have a need to use Bcrypt but only in two of my functions. So I would rather not have to load it for all of my other cloud functions where it is not needed.
In my application, I have the following import:
import BcryptTool from './classes/bcrypt'; // <--- only needed in 2 functions
Here is the contents of bcrypt.ts:
import * as bcrypt from 'bcryptjs';
export default class BcryptTool {
public static hashValue(value: string, rounds: number, callback: (error: Error, hash: string) => void) : void {
bcrypt.hash(value, rounds, (error:any, hash:any) => {
callback(error, hash);
});
}
public static compare(value: string, dbHash: string, callback: (error: string | null, match: boolean | null) => void) {
bcrypt.compare(value, dbHash, (err: Error, match: boolean) => {
if(match) {
callback(null, true);
} else {
callback('Invalid value match', null);
}
});
}
}
And finally, in my Firebase Cloud functions index.ts:
const express = require('express');
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cors = require('cors')({ origin: true });
admin.initializeApp();
const util = express();
const api = express();
...
import BcryptTool from './classes/bcrypt'; // <-- when i import here, calls to its methods within my functions work as expected
...
util.use(cors);
util.post('/verify', async (request: any, response: any) => {
// according to Doug's answer below i should be able to attempt to import here as a solution using a dynamic import expression like so:
const BcryptTool = await import('./classes/bcrypt');
// but the following subsequent call to .compare() fails
BcryptTool.compare(...)
// VS Code hinting shows an error: Property 'compare' does not exist on type 'typeof import('FULL/PATH/TO/CLASS/classes/bcrypt')'
});
api.use(cors);
api.post('/endpoint/foo', async (request: any, response: any) => {
// I do not need Bcrypt here
});
api.post('/endpoint/bar', async (request: any, response: any) => {
// I do not need Bcrypt here
});
Is this not possible? Am I just doing it all wrong?*
Sure, you can async (dynamic) import anywhere you want in TypeScript code. The imported symbols will be visible in the scope where you imported it, and nowhere else. It doesn't matter what the module contains.
(Posted answer on behalf of the question author, to move it from the question post).
I was not importing the class correctly. The reason along with a solution is outlined here.
I have created a function which basically loops over an array and create files. I'm starting to get into testing using Jest to have some extra security in place to make sure everything works however I'm experiencing some issues trying to mock the Node.js filesystem.
This is the function I wish to test - function.ts:
export function generateFiles(root: string) {
fs.mkdirSync(path.join(root, '.vscode'));
files.forEach((file) => {
fs.writeFileSync(
path.join(root, file.path, file.name),
fs.readFileSync(path.join(__dirname, 'files', file.path, file.name), 'utf-8')
);
});
}
const files = [
{ name: 'tslint.json', path: '' },
{ name: 'tsconfig.json', path: '' },
{ name: 'extensions.json', path: '.vscode' },
];
I've been reading around but can't really figure out how to test this with jest. No examples to look at. I've tried to install mock-fs which should be a simple way of getting up and running with a mock version of the Node.js FS module but I honestly don't know where to start. This is my first attempt at making a simple test - which causes an error, says 'no such file or directory' - function.test.ts:
import fs from 'fs';
import mockfs from 'mock-fs';
beforeEach(() => {
mockfs({
'test.ts': '',
dir: {
'settings.json': 'yallo',
},
});
});
test('testing mock', () => {
const dir = fs.readdirSync('/dir');
expect(dir).toEqual(['dir']);;
});
afterAll(() => {
mockfs.restore();
});
Anyone who can point me in the right direction?
Since you want to test you implementation you can try this:
import fs from 'fs';
import generateFiles from 'function.ts';
// auto-mock fs
jest.mock('fs');
describe('generateFiles', () => {
beforeAll(() => {
// clear any previous calls
fs.writeFileSync.mockClear();
// since you're using fs.readFileSync
// set some retun data to be used in your implementation
fs.readFileSync.mockReturnValue('X')
// call your function
generateFiles('/root/test/path');
});
it('should match snapshot of calls', () => {
expect(fs.writeFileSync.mock.calls).toMatchSnapshot();
});
it('should have called 3 times', () => {
expect(fs.writeFileSync).toHaveBeenCalledTimes(3);
});
it('should have called with...', () => {
expect(fs.writeFileSync).toHaveBeenCalledWith(
'/root/test/path/tslint.json',
'X' // <- this is the mock return value from above
);
});
});
Here you can read more about the auto-mocking
I need to mock 'mkdirp-promise' node module which exposes a constructor function as below
mkdirpPromise(dirPath)
.then(() => {
console.log('ABCDEFGH');
resolve(dirPath);
})
.catch((error) => {
console.log('HeABCDEFGHre');
const details = error.message;
const err = customError.failed_create_downloads_directory()
.withDetails(details);
reject(err);
});
Im able to mock it using proxiquire as below for the first time:-
let mkdirpPromiseMock = sinon.stub().rejects();
const sthreeDownloadMock =
proxyquire('./../../modules/sThreeDownload', {
joi: joiMock,
fs: fsMock,
'#monotype/core-error': {
errors: {
ApiError: customErrorMock,
},
},
'aws-sdk': awsSDK,
'mkdirp-promise': mkdirpPromiseMock,
path: pathMock,
});
Now i want to override mkdirpPromiseMock in 2nd test case with
mkdirpPromiseMock = sinon.stub().resolves();
which im not able to. Any help is appreciated.
Proxyquire is not compatible with jest.
You need to use a mocking library like rewiremock.
Please have a look at this answer which goes into detail.
REPL example