TypeScript + Jestでfsをモックする


TypeScriptでライブラリを作っていて、ユニットテスト内でfsをモックしようとしたときにやり方がわからず苦労したので残しておきます。

環境

  • Node.js

    v12.18.1

  • TypeScript

    4.0.3

  • Jest

    26.6.0

背景

GrafanaをWeb API経由で管理するライブラリのユニットテスト内でファイルを扱っていました。
以下のようなテスト対象のクラスがあるため、fsをユニットテスト内でモックするためにJestのmocking機能を利用しました。

Sample.ts
export class Sample {
    readFile(source: string): string {
      if (!fs.existsSync(source) {
          throw new Error();
      } else {
          return fs.readFileSync(source, {
              encoding: "utf8"
          });
      }
    }
}

モッキング方法

方法1

こちらの記事でも取り上げられている方法になります。
しかし、こちらではうまくいきませんでした。

Sample.test.ts
jest.mock('fs', () => ({
    readFileSync: jest.fn(() => `first¥n second¥n third`),
}));
import * as fs from "fs";
import { Sample } from "./Sample";

describe("Smaple test", () => {
    beforeEach(() => {
        // mockClearが定義されていないため、トランスパイルできない
        fs.readFileSync.mockClear();
        fs.existsSync.mockClear();
    });
    it("ファイルが存在しない", () => {
        fs.existsSync.mockImplementation(() => false);
        fs.readFileSync.mockImplementation(() => "");
        const sample = new Sample();
        expect(() => sample.readFile("foo.txt")).toThrow();
    });
    it("ファイルが空でない", () => {
        fs.existsSync.mockImplementation(() => true);
        fs.readFileSync.mockImplementation(() => "sample");
        const sample = new Sample();
        const result = sample.readFile("foo.txt");
        expect(result).toBe("sample");
    });
});

方法2

ts-jestの中にあるUtilクラスを使って、モックを行う方法になります。
ts-jestはjestをTypeScript環境でも使うためのパッケージです。Jestの公式ドキュメントでも言及されているので、安心です。

使う際に気を付けることは使用しているAPIと同様にmockImplementationを実装する点です。

Sample.test.ts
// ここでts-jestのモック作成Utilをインポート
import { mocked } from "ts-jest/utils";

// fsをモック
jest.mock('fs');
import { Sample } from "./Sample";

describe("Smaple test", () => {
    it("ファイルが存在しない", () => {
        // 対象の関数をモックする
        mocked(fs.existsSync).mockImplementation((_) => false);
        mocked(fs.readFileSync).mockImplementation((_, __) => "sample");

        const sample = new Sample();
        expect(() => sample.readFile("foo.txt")).toThrow();
    });
    it("ファイルが空でない", () => {
        // 対象の関数をモックする
        mocked(fs.existsSync).mockImplementation((_) => true);
        mocked(fs.readFileSync).mockImplementation((_, __) => "sample");

        const sample = new Sample();
        const result = sample.readFile("foo.txt");
        expect(result).toBe("sample");
    });
});

結論

ts-jestのutilsを使うのがベストです。
毎回、mockImplementationを行う必要がありそうですが(もしかしたらいらないかも)、
テストコード内で共通関数を呼び出すなど工夫をすれば、そこまで気にしないで済みそうです。