I have a small function designed to unzip a file using 'unzipper' and extract to a given location.
when unit testing the function times out, for unit testing I am using jest.
see below code :
exports.unzipFile = async (folderPath) => {
return new Promise((resolve, reject) => {
fs.createReadStream(folderPath)
.pipe(unzipper.Extract({ path: tmpPath+ path.parse(folderPath).name })).on('close', () => resolve()).on('error', (error) => reject(error))
})
The function itself works as expected. I have tried some changes to the function but this seems to break the function. I need this function to execute fully as the unzipped file is then relied on later in the program.
The program is written in node 16.
Any help would be appreciated thanks
EDIT: this is my current unit test- I have tried various things :
const { PassThrough } = require('stream')
const os = require('os');
const unzipper = require("unzipper")
const fs = require("fs")
let tmpdir, mockReadStream
beforeEach(() => {
tmpdir = os.tmpdir() + "/uploadFolder/";
if (!fs.existsSync(tmpdir)){
fs.mkdirSync(tmpdir);
}
fs.writeFileSync(tmpdir+"tempfile.zip", "file to be used")
mockReadStream = new PassThrough()
})
afterEach(() => {
// Restore mocks
jest.clearAllMocks()
})
describe('Test helper.js unzip method', () => {
test('should be able to unzip file ', async () => {
jest.isolateModules(() => {
helper = require('helper')
})
const result = await helper.unzipFile(tmpdir+"tempfile.zip")
console.log(result)
})
})
you need to make some minor changes in order to test it
in helper.js
change the signature of the unzipFile function to unzipFile = async (zipFilePath, outputDir)
const unzipper = require("unzipper")
const fs = require("fs")
const unzipFile = async (zipFilePath, outputDir) => {
return new Promise((resolve, reject) => {
fs
.createReadStream(zipFilePath)
.pipe(unzipper.Extract({ path: outputDir }))
.on('close', () => resolve())
.on('error', (error) => reject(error))
})
}
module.exports = { unzipFile }
then you need to create a simple zip file and add it locally at your test dir
in my example i created a simple zip file 1.txt.zip that contains text file with the string hello
then your test should look like this
const path = require("path");
const shelljs = require("shelljs");
const helper = require("./helper")
const fs = require("fs")
const testFilesDir = path.join(__dirname, "test");
const outPutDir = path.join(__dirname, "output")
console.log(testFilesDir)
beforeAll((done) => {
shelljs.mkdir("-p", testFilesDir);
shelljs.mkdir("-p", outPutDir);
shelljs.cp("-r", path.join(__dirname, "*.zip"), testFilesDir);
done();
});
afterAll((done) => {
shelljs.rm("-rf", testFilesDir);
done();
});
describe('Test helper.js unzip method', () => {
test('should be able to unzip file ', async () => {
await helper.unzipFile(path.join(testFilesDir, "1.txt.zip"), outPutDir)
const files = fs.readdirSync(outPutDir)
expect(files.length).toEqual(1);
expect(files[0]).toEqual("1.txt");
const text = fs.readFileSync(path.join(outPutDir,"1.txt"),"utf8")
expect(text).toEqual("hello");
})
})
Related
After trying many ways to do this, I've hit a brick wall. With the code I have now, once the fs.writeFile is dummied, the software throws the following error:
Error: ENOENT: no such file or directory (followed by the path and file).
A simple routine to write a file using fs:
'use strict';
const fs = require('fs').promises;
const path = require('path');
const writeData = async (data, file) => {
const directoryPath = path.join(__dirname, '../wiremock/stubs/mappings');
try {
fs.writeFile(`${directoryPath}/${file}`, data);
return `${file} written`
} catch (err) {
return err;
}
};
The unit test:
'use strict';
const fs = require('memfs');
const path = require('path');
jest.mock('fs');
const {
writeData,
} = require('../transform');
const directoryPath = path.join(__dirname, '../../wiremock/stubs/mappings');
describe('Write file', () => {
it('should write a file', async () => {
try {
await fs.writeFile(`${directoryPath}/test-file`, 'test data');
const result = await writeData('test data', 'test-file');
expect(result).toEqual('test-file written')
} catch (err) {
console.log('error=', err);
}
});
});
You didn't mock the fs.promises correctly.
index.js:
const fs = require('fs').promises;
const path = require('path');
const writeData = async (data, file) => {
const directoryPath = path.join(__dirname, '../wiremock/stubs/mappings');
try {
await fs.writeFile(`${directoryPath}/${file}`, data);
return `${file} written`;
} catch (err) {
return err;
}
};
module.exports = { writeData };
index.test.js:
const { writeData } = require('.');
jest.mock('fs', () => ({
promises: {
writeFile: jest.fn(),
},
}));
describe('Write file', () => {
it('should write a file', async () => {
const result = await writeData('test data', 'test-file');
expect(result).toEqual('test-file written');
});
});
Test result:
PASS stackoverflow/72115160/index.test.js
Write file
✓ should write a file (3 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.857 s, estimated 12 s
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 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 [{}];
}
};
On my local dev machine accessing localhost the following code works beautifully even with network settings changed to "Slow 3G." However, when running on my VPS, it fails to process the file on the server. Here are two different codes blocks I tried (again, both work without issue on local dev machine accessing localhost)
profilePicUpload: async (parent, args) => {
const file = await args.file;
const fileName = `user-${nanoid(3)}.jpg`;
const tmpFilePath = path.join(__dirname, `../../tmp/${fileName}`);
file
.createReadStream()
.pipe(createWriteStream(tmpFilePath))
.on('finish', () => {
jimp
.read(`tmp/${fileName}`)
.then(image => {
image.cover(300, 300).quality(60);
image.writeAsync(`static/uploads/users/${fileName}`, jimp.AUTO);
})
.catch(error => {
throw new Error(error);
});
});
}
It seems like this code block doesn't wait long enough for the file upload to finish since if I check the storage location on the VPS, I see this:
I also tried the following with no luck:
profilePicUpload: async (parent, args) => {
const { createReadStream } = await args.file;
let data = '';
const fileStream = await createReadStream();
fileStream.setEncoding('binary');
// UPDATE: 11-2
let i = 0;
fileStream.on('data', chunk => {
console.log(i);
i++;
data += chunk;
});
fileStream.on('error', err => {
console.log(err);
});
// END UPDATE
fileStream.on('end', () => {
const file = Buffer.from(data, 'binary');
jimp
.read(file)
.then(image => {
image.cover(300, 300).quality(60);
image.writeAsync(`static/uploads/users/${fileName}`, jimp.AUTO);
})
.catch(error => {
throw new Error(error);
});
});
}
With this code, I don't even get a partial file.
jimp is a JS library for image manipulation.
If anyone has any hints to get this working properly, I'd appreciate it very much. Please let me know if I'm missing some info.
I was able to figure out a solution by referring to this article: https://nodesource.com/blog/understanding-streams-in-nodejs/
Here is my final, working code:
const { createWriteStream, unlink } = require('fs');
const path = require('path');
const { once } = require('events');
const { promisify } = require('util');
const stream = require('stream');
const jimp = require('jimp');
profilePicUpload: async (parent, args) => {
// have to wait while file is uploaded
const { createReadStream } = await args.file;
const fileStream = createReadStream();
const fileName = `user-${args.uid}-${nanoid(3)}.jpg`;
const tmpFilePath = path.join(__dirname, `../../tmp/${fileName}`);
const tmpFileStream = createWriteStream(tmpFilePath, {
encoding: 'binary'
});
const finished = promisify(stream.finished);
fileStream.setEncoding('binary');
// apparently async iterators is the way to go
for await (const chunk of fileStream) {
if (!tmpFileStream.write(chunk)) {
await once(tmpFileStream, 'drain');
}
}
tmpFileStream.end(() => {
jimp
.read(`tmp/${fileName}`)
.then(image => {
image.cover(300, 300).quality(60);
image.writeAsync(`static/uploads/users/${fileName}`, jimp.AUTO);
})
.then(() => {
unlink(tmpFilePath, error => {
console.log(error);
});
})
.catch(error => {
console.log(error);
});
});
await finished(tmpFileStream);
}
i have a single endpoint where i call twice on 2 different describes to test different responses
const express = require("express");
const router = express.Router();
const fs = require("fs");
const multer = require("multer");
const upload = multer({ dest: "files/" });
const csv = require("fast-csv");
let response = { message: "success" }
router.post("/post_file", upload.single("my_file"), (req, res) => {
let output = get_output(req.file.path);
fs.unlinkSync(req.file.path);
if(output.errors.length > 0) response.message = "errors found";
res.send(JSON.stringify(response))
})
const get_output = (path) => {
let errors = []
let fs_stream = fs.createReadStream(path);
let csv_stream = csv.parse().on("data", obj => {
if(!is_valid(obj)) errors.push(obj);
});
fs_stream.pipe(csv_stream);
return {errors};
}
const is_valid = (row) => {
console.log("validate row")
// i validate here and return a bool
}
my unit tests
const app = require("../server");
const supertest = require("supertest");
const req = supertest(app);
describe("parent describe", () => {
describe("first call", () => {
const file = "my path to file"
// this call succeeds
it("should succeed", async (done) => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("success")
done();
});
})
describe("second call", () => {
const file = "a different file"
// this is where the error starts
it("should succeed", async (done) => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("errors found")
done();
});
})
})
// csv file is this
NAME,ADDRESS,EMAIL
Steve Smith,35 Pollock St,ssmith#emailtest.com
I get the following
Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "validate row".
The problem is that tested route is incorrectly implemented, it works asynchronously but doesn't wait for get_output to end and responds synchronously with wrong response. The test just reveals that console.log is asynchronously called after test end.
Consistent use of promises is reliable way to guarantee the correct execution order. A stream needs to be promisified to be chained:
router.post("/post_file", upload.single("my_file"), async (req, res, next) => {
try {
let output = await get_output(req.file.path);
...
res.send(JSON.stringify(response))
} catch (err) {
next(err)
}
})
const get_output = (path) => {
let errors = []
let fs_stream = fs.createReadStream(path);
return new Promise((resolve, reject) => {
let csv_stream = csv.parse()
.on("data", obj => {...})
.on("error", reject)
.on("end", () => resolve({errors}))
});
}
async and done shouldn't be mixed in tests because they serve the same goal, this may result in test timeout if done is unreachable:
it("should succeed", async () => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("success")
});