Is there a way to easily compare 2 ast's on babel?
Consider a program like this as the source file:
// example.ts
import { foo } from "bar";
describe("Some test", () => {
let moduleFixture: any;
beforeAll(async () => {
moduleFixture = await foo.createTestingModule({}).compile();
});
afterAll(async () => {
await foo.tearDown();
});
});
And consider the following babel program
const babelParser = require("#babel/parser");
const { default: traverse } = require("#babel/traverse");
const { readFileSync } = require("fs");
const recast = require("recast");
const { default: template } = require("#babel/template");
const source = readFileSync(`./example.ts`, "utf-8");
const buildBeforeAll = template(
` beforeAll(async () => {
moduleFixture = await foo.createTestingModule({}).compile();
}); `,
{
plugins: ["typescript"],
}
);
const beforeAllAst = buildBeforeAll();
const ast = babelParser.parse(source, {
allowImportExportEverywhere: true,
plugins: ["typescript"],
});
traverse(ast, {
enter(path) {
const isBeforeAll = path.isIdentifier({ name: "beforeAll" });
if (isBeforeAll) {
// Somehow compare is beforeAllASt === path
console.log(`found an appropriate beforeall`);
path.replaceWithSourceString(`beforeEach`);
}
},
});
console.log(recast.print(ast).code);
What would be the best way to compare beforeAllAst with a traversed node?
The simplest way to made such modifications will be using 🐊Putout code transformer, I'm working on.
Here is how it looks like:
const source = `
beforeAll(async () => {
moduleFixture = await foo.createTestingModule({}).compile();
});
`;
module.exports.replace = () => ({
[source]: (vars, path) => {
path.node.callee.name = 'beforeEach'
return path;
},
});
This plugin searches for exact mach of beforeAll function call. It uses #putout/compare which can be used directly in your code:
const {compare} = require('#putout/compare');
traverse(ast, {
enter(path) {
const isBeforeAll = path.isIdentifier({ name: "beforeAll" });
if (compare(path, beforeAllAst)) {
// Somehow compare is beforeAllASt === path
console.log(`found an appropriate beforeall`);
path.replaceWithSourceString(`beforeEach`);
}
},
});
#putout/compare compares AST-node with given string or other AST-node
Related
I have used awilix to be able to have dependency injection in javascript to be able to have easier test. but now I want to mock a resolver that is set in my container for only a set of tests
In other words, I have a resolver that I want to mock it in my test for some reasons, (it is costly to call it so many times and it is a time consuming network call.) thus, I need to mock it in many of my tests for example in a test which is called called b.test.js, but I want it to call the actual function in a.test.js
here is my awilix config
var awilix = require('awilix');
var container = awilix.createContainer({
injectionMode: awilix.InjectionMode.PROXY,
});
var network = () => {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve('data') }, 3000);
});
}
module.exports = container.register({ network: awilix.asValue(network) });
my test is
const container = require('../container');
container.register({
heavyTask: awilix.asValue(mockFunction),
});
describe('b', () => {
it('b', async () => {
const result = await container.resolve('network')();
});
});
somehow you've already done it
but don't config container like what you've done, because this way you're gonna have a single object of container, so if you change that object it'll be changed in all tests, instead do it this way
const awilix = require('awilix');
const heavyTask = () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve('actual run');
}, 3000);
});
const configContainer = () => {
const container = awilix.createContainer({
injectionMode: awilix.InjectionMode.PROXY,
});
return container.register({
heavyTask: awilix.asValue(heavyTask),
});
}
module.exports = configContainer;
it seems you already know that you can overwrite your registrations, which might be the only vague part
so a.test.js can be written as
const { describe, it } = require('mocha');
const { expect } = require('chai');
const configContainer = require('../container');
const container = configContainer();
describe('a', () => {
it('a', async () => {
const res = await container.resolve('heavyTask')();
expect(res).to.eq('actual run');
});
});
and test b can be written as something like this
const awilix = require('awilix');
const { describe, it } = require('mocha');
const { expect } = require('chai');
const configContainer = require('../container');
const container = configContainer();
const heavyTask = () => 'mock run';
container.register({
heavyTask: awilix.asValue(heavyTask),
});
describe('b', () => {
it('b', async () => {
const res = await container.resolve('heavyTask')();
expect(res).to.eq('mock run');
});
});
I have developed the node js code as MVC architecture. The folder structure is Controller --> service -> model. And I have tried to write unit testing for the following code. Unfortunately, I couldn't mock the service function. So please help me to resolve it.
Controller
const SubscriberService = require('../../services/subscriber/subscriberService')
const response = require("../../config/response");
const constant = require('../../config/constant');
const SubscriberAnswerService = require('../../services/subscriberAnswer/subscriberAnswerService');
const path = require('path');
class SubscriberController {
constructor() {
this.subscriberService = new SubscriberService();
this.subscriberAnswerService = new SubscriberAnswerService();
}
async getSubscriber(req, res) {
try {
var { userId } = req;
const user = await this.subscriberService.findByUserId(userId);
if (user != null) {
res.send(response.res(true, constant.MSG.USER_DETAILS, user))
} else {
res.status(404).send(response.res(false, constant.MSG.USER_NOT_FOUND));
}
} catch (error) {
res.status(constant.RESPONSE.INTERNAL_ERROR.CODE)
.send(response.res(false, error.message))
}
}
}
Service
async findByUserId(id) {
const user = await Subscriber.findOne({ where: { id: id, status: 1 } });
return user;
}
Unit Testing Code
describe("Test SubscriberController", () => {
it("Test getsubscriber", async () => {
req.userId = 1;
jest.spyOn(subscriberService, "findByUserId").mockReturnValue(subscriberResponse);
await subscriberController.getSubscriber(req, res);
expect(res.statusCode).toBe(500);
});
});
Issue: I have mocked the service function which findByUserId but it does not work. It is given the following error.
error TypeError: Cannot read property 'findOne' of undefined
Please give the solution to mock findByUserId function.
Subscriber.Controller.test.js
const subscriberModel = require("../src/models/subscriber/subscriberModel");
const SubscriberService = require("../src/services/subscriber/subscriberService");
const SubscriberController = require("../src/controllers/Subscriber/subscriberController");
const subscriberController = new SubscriberController();
const subscriberService = new SubscriberService();
const httpMocks = require("node-mocks-http");
jest.mock("../src/models/subscriber/subscriberModel");
beforeEach(() => {
jest.resetAllMocks();
req = httpMocks.createRequest();
res = httpMocks.createResponse();
next = jest.fn();
jest.resetAllMocks();
subscriberModel.findOne = jest.fn();
});
// subscriberService.findByUserId = jest.fn();
const subscriberResponse = {
id: 1,
name: 'Sandun',
msisdn: '94704377575',
otp: '1234',
deleted: 0,
attempts: 0,
img_url: 'https://'
}
jest.mock('../src/models/subscriber/subscriberModel', () => () => {
const SequelizeMock = require("sequelize-mock");
let dbMock = new SequelizeMock();
let subscriberMock = dbMock.define('subscribers', {
id: 1,
name: 'Sandun',
msisdn: '94704377575',
otp: '1234',
deleted: 0,
attempts: 0,
img_url: 'https://'
});
let groupMock = dbMock.define('winner', {});
subscriberMock.belongsTo(groupMock);
subscriberMock.hasMany();
});
// This test shows how the constructor can be mocked, and how to spy on passed parameters.
describe("Test SubscriberController", () => {
it("Test getsubscriber", async () => {
req.userId = 1;
jest.spyOn(subscriberService, "findByUserId").mockReturnValue(subscriberResponse);
await subscriberController.getSubscriber(req, res);
expect(res.statusCode).toBe(200);
});
});
I am trying to mock the promise version of fs.writeFile using Jest, and the mocked function is not being called.
Function to be tested (createFile.js):
const { writeFile } = require("fs").promises;
const createNewFile = async () => {
await writeFile(`${__dirname}/newFile.txt`, "Test content");
};
module.exports = {
createNewFile,
};
Jest Test (createFile.test.js):
const fs = require("fs").promises;
const { createNewFile } = require("./createFile.js");
it("Calls writeFile", async () => {
const writeFileSpy = jest.spyOn(fs, "writeFile");
await createNewFile();
expect(writeFileSpy).toHaveBeenCalledTimes(1);
writeFileSpy.mockClear();
});
I know that writeFile is actually being called because I ran node -e "require(\"./createFile.js\").createNewFile()" and the file was created.
Dependency Versions
Node.js: 14.1.0
Jest: 26.6.3
-- Here is another attempt at the createFile.test.js file --
const fs = require("fs");
const { createNewFile } = require("./createFile.js");
it("Calls writeFile", async () => {
const writeFileMock = jest.fn();
jest.mock("fs", () => ({
promises: {
writeFile: writeFileMock,
},
}));
await createNewFile();
expect(writeFileMock).toHaveBeenCalledTimes(1);
});
This throws the following error:
ReferenceError: /Users/danlevy/Desktop/test/src/createFile.test.js: The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
Invalid variable access: writeFileMock
Since writeFile is destructured at import time instead of being consistently referred as fs.promises.writeFile method, it cannot be affected with spyOn.
It should be mocked as any other module:
jest.mock("fs", () => ({
promises: {
writeFile: jest.fn().mockResolvedValue(),
readFile: jest.fn().mockResolvedValue(),
},
}));
const fs = require("fs");
...
await createNewFile();
expect(fs.promises.writeFile).toHaveBeenCalledTimes(1);
It make sense to mock fs scarcely because unmocked functions provide side effects and potentially have negative impact on test environment.
Mock "fs/promises" async functions in jest
Here is a simple example using fs.readdir(), but it would also apply to any of the other async fs/promises functions.
files.service.test.js
import fs from "fs/promises";
import FileService from "./files.service";
jest.mock("fs/promises");
describe("FileService", () => {
var fileService: FileService;
beforeEach(() => {
// Create a brand new FileService before running each test
fileService = new FileService();
// Reset mocks
jest.resetAllMocks();
});
describe("getJsonFiles", () => {
it("throws an error if reading the directory fails", async () => {
// Mock the rejection error
fs.readdir = jest.fn().mockRejectedValueOnce(new Error("mock error"));
// Call the function to get the promise
const promise = fileService.getJsonFiles({ folderPath: "mockPath", logActions: false });
expect(fs.readdir).toHaveBeenCalled();
await expect(promise).rejects.toEqual(new Error("mock error"));
});
it("returns an array of the .json file name strings in the test directory (and not any other files)", async () => {
const allPotentialFiles = ["non-json.txt", "test-json-1.json", "test-json-2.json"];
const onlyJsonFiles = ["test-json-1.json", "test-json-2.json"];
// Mock readdir to return all potential files from the dir
fs.readdir = jest.fn().mockResolvedValueOnce(allPotentialFiles);
// Get the promise
const promise = fileService.getJsonFiles({ folderPath: "mockPath", logActions: false });
expect(fs.readdir).toBeCalled();
await expect(promise).resolves.toEqual(onlyJsonFiles); // function should only return the json files
});
});
});
files.service.ts
import fs from "fs/promises";
export default class FileService {
constructor() {}
async getJsonFiles(args: FilesListArgs): Promise<string[]> {
const { folderPath, logActions } = args;
try {
// Get list of all files
const files = await fs.readdir(folderPath);
// Filter to only include JSON files
const jsonFiles = files.filter((file) => {
return file.includes(".json");
});
return jsonFiles;
} catch (e) {
throw e;
}
}
}
I know this is an old thread, but in my case, I wanted to handle different results from readFile (or writeFile in your case). So I used the solution Estus Flask suggested with the difference that I handle each implementation of readFile in each test, instead of using mockResolvedValue.
I'm also using typescript.
import { getFile } from './configFiles';
import fs from 'fs';
jest.mock('fs', () => {
return {
promises: {
readFile: jest.fn()
}
};
});
describe('getFile', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should return results from file', async () => {
const mockReadFile = (fs.promises.readFile as jest.Mock).mockImplementation(async () =>
Promise.resolve(JSON.stringify('some-json-value'))
);
const res = await getFile('some-path');
expect(mockReadFile).toHaveBeenCalledWith('some-path', { encoding: 'utf-8' });
expect(res).toMatchObject('some-json-value');
});
it('should gracefully handle error', async () => {
const mockReadFile = (fs.promises.readFile as jest.Mock).mockImplementation(async () =>
Promise.reject(new Error('not found'))
);
const res = await getFile('some-path');
expect(mockReadFile).toHaveBeenCalledWith('some-path', { encoding: 'utf-8' });
expect(res).toMatchObject('whatever-your-fallback-is');
});
});
Note that I had to cast fs.promises.readFile as jest.Mock in order to make it work for TS.
Also, my configFiles.ts looks like this:
import { promises as fsPromises } from 'fs';
const readConfigFile = async (filePath: string) => {
const res = await fsPromises.readFile(filePath, { encoding: 'utf-8' });
return JSON.parse(res);
};
export const getFile = async (path: string): Promise<MyType[]> => {
try {
const fileName = 'some_config.json';
return readConfigFile(`${path}/${fileName}`);
} catch (e) {
// some fallback value
return [{}];
}
};
disclaimer: this is part of a course I am taking and a practice task. I am having quite hard time wrapping my head around the test chapter of a course I am taking. Given the following class, I have to write test to it but I see the error else path not taken on my return results; line. Which else is it talking about?
import DB from './DB';
import ErrorLogger from './ErrorLogger'; // ===> if path not taken here
class ChapterSevenJest {
constructor() {
if (!ChapterSevenJest.instance) {
ChapterSevenJest.instance = this;
}
return ChapterSevenJest.instance;
}
_db = new DB();
getData = async (gradeId, teamId) => {
let results = [];
try {
results = await this._db.getData(gradeId, teamId);
} catch (error) {
ErrorLogger.register(
error
);
}
return results; // else path not taken here
};
}
const JestPractice = new ChapterSevenJest();
export default JestPractice;
the test:
import DB from './DB';
import ErrorLogger from './ErrorLogger';
import JestPractice from './JestPractice';
describe('service', () => {
const gradeId = 11;
const teamId = 1;
let spyLogs;
beforeEach(() => {
spyLogs = jest.spyOn(ErrorLogger, 'register');
spyLogs.mockReturnValue(true);
});
afterEach(() => {
spyLogs.mockReset();
});
it('should return data ot a grade and team', async () => {
const spyDB = jest.spyOn(
DB.prototype,
'getData'
);
const stat = [
{
"score" : 100,
"rank": 2
}
]
spyDB.mockResolvedValue(stat);
const results = await JestPractice.getData(
gradeId,
teamId
);
expect(spyDB).toHaveBeenCalledWith(gradeId, teamId);
expect(results).toHaveLength(1);
expect(results[0].score).toStrictEqual(100);
expect(results[0].rank).toStrictEqual(2);
});
it('should return empty on error', async () => {
const spyDB = jest.spyOn(
DB.prototype,
'getData'
);
spyDB.mockRejectedValue('error');
const results = await JestPractice.getData(
gradeId,
teamId
);
expect(spyDB).toHaveBeenCalledWith(gradeId, teamId);
expect(results).toHaveLength(0);
expect(spyLogs).toHaveBeenCalledWith(
"error"
);
});
});
There is only 1 if statement which is
if (!ChapterSevenJest.instance) {
ChapterSevenJest.instance = this;
}
You need to have a test where ChapterSevenJest.instance is truthy so that the if block doesn't execute.
I can't understand at all the utility of mockings. See, I have the next module:
function Code() {
this.generate = () => {
const result = 'code124';
return result;
};
}
module.exports = Code;
Now, I want to test it with jest:
const Code = require('../lib/code');
jest.mock('../lib/code', () => {
return jest.fn().mockImplementation(() => {
return {
generate: () => [1, 2, 3]
};
});
});
describe('Code', () => {
test('returns the code "code123"', () => {
const code = new Code();
expect(code.generate()).toBe('code123');
});
});
So... This test will be fine but... My code ain't so... what's the point about mocking if I can set a correct result even though my code is wrong?
You're NOT supposed to mock the unit you're testing. You're supposed to mock it's dependencies.
for example:
whenever you have a dependency in the implementation:
const dependency = require('dependency');
function Code() {
this.generate = () => {
const result = 'code' + dependency.getNumber();
return result;
};
}
module.exports = Code;
you'll be able to modify it's results to be able to test all scenarios without using the actual implementation of your dependency:
const dependency = require('dependency');
const Code = require('../lib/code');
jest.mock('dependency');
describe('Code', () => {
describe('when dependency returns 123', () => {
beforeAll(() => {
dependency.getNumber.mockReturnValue('123');
});
it('should generate code123', () => {
const code = new Code();
expect(code.generate()).toEqual('code123');
});
});
describe('when dependency returns 124', () => {
beforeAll(() => {
dependency.getNumber.mockReturnValue('124');
});
it('should generate code123', () => {
const code = new Code();
expect(code.generate()).toEqual('code124');
});
});
});