Search code examples
javascripttypescriptvitest

Exporting object keys individually


I want to mock fs in vitest using memfs. And for that I created a mock file ./__mocks__/fs.ts and set up the mocked volume and fs.

However, I cannot get the mocked export to work properly. I always get TypeError: readdirSync is not a function.

What is the correct way to export this?

__mocks_/fs.ts:

// fs.ts
import { createFsFromVolume, Volume } from "memfs"

const musicFolder = "./musicFolder"
const files = {
  "./1.mp3": "1",
  "./2.mp3": "2",
}

const volume = Volume.fromJSON(files, musicFolder)

const fs = createFsFromVolume(volume)

export { fs } // This does not work

Test file:

// test.spec.ts
import { describe, expect, it, vi } from "vitest"
import * as fs from "fs"

vi.mock("fs")

it("testing fs", async () => {
  const files = fs.readdirSync("./musicFolder") //! TypeError: readdirSync is not a function

  expect(files).toBeTruthy()
})

Solution

  • You've run into the problem of importing an entire module's contents under a namespace and the requirement to explicitly define each export by name. Using ES modules, there's no way around this.

    IMO, part of the problem is that you are utilizing mixed implicit behavior provided by vitest, and explicit behavior in your code. It's easier for me to reason about code when it is explicit. Consider this alternative to the vi.mock method:

    For this example, let's say that the module you're testing is at ./src/example.ts.

    Mock module

    Create a mock module alongside your test module (which I'll describe next):

    I'm using the convention of [name].test.mock.[ext]

    // ./src/example.test.mock.ts
    
    import { createFsFromVolume, type IFs, Volume } from 'memfs';
    
    // A factory to produce the mocked fs
    export function createFs (): IFs {
      const files = {
        './1.mp3': '1',
        './2.mp3': '2',
      };
    
      const dir = './musicFolder';
      const volume = Volume.fromJSON(files, dir);
      return createFsFromVolume(volume);
    }
    
    // Or export an instance of it directly
    // if it's the only one that you actually need
    export const fs = createFs();
    
    

    Test module

    I'm using the convention of [name].test.[ext]

    Here are two ways you can write it:

    First version

    // ./src/example.test.ts
    
    import { expect, it } from 'vitest';
    
    // If the real `fs` from Node is needed elsewhere in your test,
    // then keep this import.
    import * as fs from 'node:fs';
    
    import { createFs } from './example.test.mock.js';
    
    it('testing fs', async () => {
      const fs = createFs();
      const files = fs.readdirSync('./musicFolder');
      expect(files).toBeTruthy();
    });
    
    // Use the real `fs` in other tests...
    
    

    Second version

    If you don't need the real fs in your test, then just use the mocked version directly:

    // ./src/example.test.ts
    
    import { expect, it } from 'vitest';
    import { fs } from './example.test.mock.js';
    
    it('testing fs', async () => {
      const files = fs.readdirSync('./musicFolder');
      expect(files).toBeTruthy();
    });
    
    

    Summary

    Both of those versions of the test run without error and pass:

    so-72860426 % node --version
    v16.15.1
    
    so-72860426 % npm run test
    
    > so-72860426@0.1.0 test
    > vitest
    
    
     DEV  v0.17.0 /stack-overflow/so-72860426
    
     ✓ src/example.test.ts (1)
    
    Test Files  1 passed (1)
         Tests  1 passed (1)
          Time  906ms (in thread 34ms, 2666.13%)
    
    
     PASS  Waiting for file changes...
           press h to show help, press q to quit
    

    And here are my repo config files so that you can reproduce this:

    ./package.json:

    {
      "name": "so-72860426",
      "version": "0.1.0",
      "description": "",
      "type": "module",
      "scripts": {
        "test": "vitest"
      },
      "keywords": [],
      "author": "",
      "license": "MIT",
      "devDependencies": {
        "@types/node": "^18.0.1",
        "typescript": "^4.7.4",
        "vitest": "^0.17.0"
      },
      "dependencies": {
        "memfs": "^3.4.7"
      }
    }
    
    

    ./tsconfig.json:

    {
      "compilerOptions": {
        "esModuleInterop": true,
        "exactOptionalPropertyTypes": true,
        "isolatedModules": true,
        "lib": [
          "esnext"
        ],
        // "jsx": "react-jsx",
        "module": "esnext",
        "moduleResolution": "nodenext",
        "noUncheckedIndexedAccess": true,
        "strict": true,
        "target": "esnext",
        "useUnknownInCatchVariables": true
      },
      "include": [
        "src/**/*"
      ]
    }