0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vitest でも静的ファクトリメソッドを持つクラスをモックしたい

Last updated at Posted at 2025-04-02

以下のような ES6 / TypeScript のクラス構文で定義された Klass クラスと、それを利用する viaConstructer() 関数があったとします。

Klass.ts
export class Klass {
    field: string;

    constructor() {
        console.log("constructor() called");
        this.field = "initialzed";
    }
}
main.ts
import { Klass } from "./Klass";

export function viaConstructer() {
    const klass = new Klass();
    console.log("viaConstructer called:", klass, klass.field);
};

Vitest で viaConstructer() 関数のテストコードを作成するにあたり、Klass クラスをモックするにはどうすればよいでしょうか?

この例では、クラスを宣言しているモジュールをモックすることで、クラス全体をモックに置き換られます。ES6 / TypeScript のクラスは糖衣構文であり実体として関数なので、モックは普通に vi.fn() で宣言できます:

import { test, vi } from "vitest"
import { viaConstructer } from "./main";

test("コンストラクタを含め、クラス全体をモックする", () => {

    vi.mock("./Klass", () => {
        return {
            Klass: vi.fn().mockImplementation(() => console.log("mocked Klass"))
        }
    });

    viaConstructer();
});

このテストを実行すると、Klass のコンストラクタが呼び出されておらず、無事にモック出来たことを確認できます。

stdout | main.test.ts > コンストラクタを含め、クラス全体をモックする
mocked Klass
viaConstructer called: spy {} undefined

静的ファクトリメソッドでもモックしたい

ではこのように、スタティックなファクトリメソッド create() と、それを利用する viaCreate() があった場合はどうでしょう。

Klass.ts
export class Klass {
    field: string;

    private constructor() {
        console.log("constructor() called");
        this.field = "initialzed";
    }

    static create(): Klass {
        console.log("create() called");
        return new Klass();
    }
}
main.ts
import { Klass } from "./Klass";

export function viaCreate() {
    const klass = Klass.create();
    console.log("viaCreate called:", klass, klass.field);
};

元のコードのままだと、このように create() が関数ではないということで怒られてしまいます。

 FAIL  viaConstructer.test.ts > コンストラクタを含め、クラス全体をモックする
TypeError: Klass.create is not a function
 ❯ Module.viaCreate main.ts:4:25
      7| 
      8| export function viaCreate() {
      9|     const klass = Klass.create();
       |                         ^
     10|     console.log("viaCreate called:", klass, klass.field);
     11| };
 ❯ main.test.ts:12:5

Klass.createundefined なので当然といえば当然です。

そこで、クラス全体を vi.fn() に置き換えるのではなく、ファクトリメソッドと同名の関数をプロパティとして持つオブジェクトに置き換えてあげます。

import { test, vi } from "vitest"
import { viaCreate } from "./main";

test("スタティックなファクトリメソッドをモックする", () => {

    vi.mock("./Klass", () => {
        return {
            Klass: {
                create: vi.fn().mockImplementation(() => {
                    console.log("mocked Klass.create()")

                    return {}
                })
            }
        }
    });

    viaCreate();
});

無事ファクトリメソッドがモックされたことを確認できますね。

stdout | viaCreate.test.ts > スタティックなファクトリメソッドをモックする
mocked Klass.create()
viaCreate called: {} undefined

また、モック化されたファクトリメソッドの返り値としてフィールド(やメソッド定義)を含めることも可能です。

- return {}
+ return { field: "mocked field" }
stdout | viaCreate.test.ts > スタティックなファクトリメソッドをモックする
mocked Klass.create()
viaCreate called: { field: 'mocked field' } mocked field

スタティックなファクトリメソッドでも呼び出し回数をチェックしたい

ファクトリメソッドに対する呼び出しを検証したい場合は、モック関数を vi.hoisted() で定義して変数に保持しておくことで expect() の引数として参照できるようになります。ES Modules の static import はトップレベルで評価される都合、vi.hoisted() で宣言した変数でなければモック関数内で参照できません。

import { test, vi, expect } from "vitest"
import { viaCreate } from "./main";

const { create } = vi.hoisted(() => {
    return {
        create: vi.fn().mockImplementation(() => {
            return {}
        })
    }
})

test("スタティックなファクトリメソッドをモックし、呼び出し回数をチェックする", () => {

    vi.mock("./Klass", () => {
        return {
            Klass: {
                create
            }
        }
    });

    viaCreate();

    expect(create).toBeCalledTimes(1);
});

まとめ

簡単に検索した範囲では Vitest で静的ファクトリメソッドを持つクラスをモックする方法を解説した記事が見つからなかったため執筆した本稿ですが、あまり直感的なコードにできませんでした。静的ファクトリメソッドには「インスタンスの取得方法をわかりやすい名前で表現できる」等のメリットがある一方、テスタビリティ(テストコードの読みやすさ)の観点では優れていると言い難いですね。同様のメリットを得たい場合、クラス外に別途ビルダーメソッドを設けることで vi.spyOn() による一般的なモック方法に寄せることが可能です:

Klass.ts
export class Klass {
    field: string;

    constructor() {
        console.log("constructor() called");
        this.field = "initialzed";
    }
}

export function klassBuilder() {
    console.log("klassBuilder() called");
    return new Klass();
};
main.ts
import { klassBuilder } from "./Klass";

export function viaBuilder() {
    const klass = klassBuilder();
    console.log("viaBuilder called:", klass, klass.field);
};
import { test, vi } from "vitest"
import { viaBuilder } from "./main";
import * as klassModule from "./Klass";

test("ビルダーメソッドをモックする", () => {

    vi.spyOn(klassModule, "klassBuilder").mockImplementation(() => {
        console.log("mocked klassBuilder")

        return { field: "mocked field" }
    })

    viaBuilder();
});
stdout | viaBuilder.test.ts > ビルダーメソッドをモックする
mocked klassBuilder
viaBuilder called: { field: 'mocked field' } mocked field

なお、ソースコードはこちらのリポジトリで公開中です。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?