Mocking ES6 Modules in Node Unit Tests
If you want to ditch Common JS modules for ES6 modules in your backend node.js code, you may want to reconsider. If you're doing test driven development, you will notice pretty fast why, but if your writing unit tests after your production code it may take you a little longer. Eventually you will realize that your trusted, tried and true testing frameworks will fail you as soon as you need to mock something like fs, process, or any functions native to node. Give it a try using mocha or jest and you will understand.
While older testing frameworks do not seem concerned with support for ES6 modules, the newer ones generally have a front end focus. Fortunately, the makers of vite.js have provided a testing framework that works well on the front end and the back end. vitest.js provides the functionality to mock ES6 modules in your back end code, and in this post, I provide a HOWTO.
Of course, the first thing to do is add vitest to the dev dependencies of your project.
npm install vitest -D
I use npm here, but you can certainly use yarn if that is your manager of choice.
It is also worth noting here that vitest will run most test files out of the box without any additional configuration. Also, it defaults to continue running and watching for file changes. So, in your package.json file all you need to add for running tests via "scripts" is:
"scripts":{
"test":"vitest"
}
Now lets make a file for testing vitest's ability to run our unit tests. It is a simple file that uses some methods from fs. We will call it useFs.js
import { stat, writeFile } from "fs/promises";
const getStat = async (root, data) => {
try {
const stats = await stat(root);
await writeFile(root, data);
return stats;
} catch (error) {
return error;
}
};
export default getStat;
This file should be easy enough to follow, but there is one important line to point out. And that is the import statement. You will notice we are using "named imports". I think this is better than simply using "import * as fs" because it shows clearly what you need to mock at the top of the file. I think this is also a good practice for all the code we right for the same reason.
So as can be clearly seen by the use of named imports we need to mock two functions: writeFile and stat. What these functions do, does not matter when we test our file but how and if our file uses them does matter. So, we mock them to keep side effects out of our tests.
Now on to the test file which we are calling useFS.spec.js. (One of the common naming conventions used for test files. We could be just as successful using useFS.test.js.)
领英推荐
import { describe, expect, it, vi, afterEach } from "vitest";
import { stat, writeFile } from "fs/promises";
import useFs from "./useFs.js";
describe("getStat", () => {
vi.mock("fs/promises", (importOriginal) => {
const mod = importOriginal();
return {
...mod,
stat: vi.fn(),
writeFile: vi.fn(),
};
});
it("should call fs.stat with the root argument", async () => {
const root = "root";
vi.mocked(stat).mockReturnValue({ size: 100 });
const result = await useFs(root);
expect(stat).toHaveBeenCalledWith(root);
expect(stat).toHaveReturnedWith({ size: 100 });
});
it("should call writeFile one time root data arguments", async () => {
const data = "write this stuff";
vi.mocked(writeFile);
const result = await useFs("path.to.file", data);
expect(writeFile).toHaveBeenCalledOnce;
expect(writeFile).toHaveBeenCalledWith("path.to.file", data);
});
});
vitest is a full featured framework with assertion libraries and many other utilities included and available for import. As you can see here, we need to import what we intend to use.
If you are familiar with unit testing, all but one of these imports are already known to you. The tool we use in vitest to mock modules is "vi".
For the remainder of this article, I will make some points about the use of the vi method.
vi.mock("fs/promises", (importOriginal) => {
const mod = importOriginal();
return {
...mod,
stat: vi.fn(),
writeFile: vi.fn(),
};
});
Here you will notice that at the top of our describe function, we mock the entire file by referencing "fs/promises" as the first argument to vi.mock. But fortunately, we use the single argument in the second argument to vi.mock (importOriginal) to include all the native methods of fs in our mock. Executing this parameter returns all the methods of fs, so by storing them in mod and then using it with the spread operator in our return object we are able to include all the native functions in our mock. Then we override specific methods by adding them here in the same object. In this case they are stat and writeFile.
Using another method of vi, vi.fn(), we create the mock for each function. Great! That is a nice clean way to create our mock. All that is left now is to call the mocks in our "it" functions using vi.mocked.
vi.mocked(stat).mockReturnValue({ size: 100 });
vi.mocked(writeFile);
You may notice we call the mockReturnValue function with the results of vi.mocked as its input. It is not needed, but we are using it here to demonstrate how to further define the mocked function at this point in the test. All that is required is to call vi.mocked with named function name.
expect(stat).toHaveBeenCalledWith(root);
expect(stat).toHaveReturnedWith({ size: 100 });
expect(writeFile).toHaveBeenCalledOnce;
expect(writeFile).toHaveBeenCalledWith("path.to.file", data);
Here we call expect with the reference to the method which returns the properties and methods of the mock and call our assertions on it. The rest of the test file should be clear to all, so no explanation will be given.
In conclusion, vitest is an all-inclusive testing framework, but also allows you to just import the parts that you desire to use. And if you are using ES6 modules in server side or non-web-based node applications it is the best testing framework that I know of for writing unit tests.
As always, comments and questions are welcome.
Engineer intern at Griffin Global Technologies
1 周I have been using jest but when it turns to ES6, it fails, I'm glad I found this, thank you.