Testing modules that interact with the filesystem can be tricky. Typically you mock indivdual methods of the fs
module, but his can be a bit tedious if you have to mock a lot of different calls. The mock-fs module streamlines this by letting you provide a simple mapping of paths to file contents and it mostly works. However if your code uses dynamic requires, you need to ensure the required files are all present in your mock filesystem.
This post shows an alternative method using unionfs and memfs. The advantage of this method is that it allows you to overlay your mock over the actual filesystem, ensuring that dynamic requires continue to work as expected.
The example module we want to test exports a catFiles
function that reads all the files in a directory and concatenates their content:
import * as readdirp from "readdirp"
import * as fs from "fs"
export async function catFiles(dir: string) {
const files = await readdirp.promise(dir)
const fileContents = await Promise.all(
files.map((file) =>
fs.promises.readFile(file.fullPath, { encoding: "utf-8" })
)
)
return fileContents.join("\n")
}
To mock the filesystem we replace the fs
module's implementation with unionfs
. unionfs
combines different fs modules into a single filesystem, looking up files in the order of its composing fs modules. union.ts#promiseMethod shows how this works under the hood: it tries to call the fs method on each of its filesystems in order until one succeeds.
Initially we setup unionfs
with just the standard fs
module:
jest.mock(`fs`, () => {
const fs = jest.requireActual(`fs`)
const unionfs = require(`unionfs`).default
return unionfs.use(fs)
})
In our test setup we then create an in-memory filesystem using memfs
with the filesystem contents to use as our mock and add it to our union filesystem:
import { Volume } from "memfs"
...
const vol = Volume.fromJSON(
{
"global.css": "html { background-color: green; }",
"style.css": "body: {color: red;}",
},
"/tmp/www"
)
fs.use(vol)
Complete example
cat-file.test.ts:
jest.mock(`fs`, () => {
const fs = jest.requireActual(`fs`)
const unionfs = require(`unionfs`).default
unionfs.reset = () => {
// fss is unionfs' list of overlays
unionfs.fss = [fs]
}
return unionfs.use(fs)
})
import * as fs from "fs"
import { Volume } from "memfs"
import { catFiles } from "./cat-files"
afterEach(() => {
// Reset the mocked fs
;(fs as any).reset()
})
test("it reads the files in the folder", async () => {
// Setup
const vol = Volume.fromJSON(
{
"global.css": "html { background-color: green; }",
"style.css": "body: {color: red;}",
},
"/tmp/www"
)
const fsMock: any = fs
fsMock.use(vol)
// Act
const combinedText = await catFiles("/tmp/www")
// Verify
expect(combinedText).toEqual(
"html { background-color: green; }\nbody: {color: red;}"
)
})
Top comments (1)
Is it possible to use fs-extra instead of fs native module?