以下のような ES6 / TypeScript のクラス構文で定義された Klass
クラスと、それを利用する viaConstructer()
関数があったとします。
export class Klass {
field: string;
constructor() {
console.log("constructor() called");
this.field = "initialzed";
}
}
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()
があった場合はどうでしょう。
export class Klass {
field: string;
private constructor() {
console.log("constructor() called");
this.field = "initialzed";
}
static create(): Klass {
console.log("create() called");
return new Klass();
}
}
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.create
は undefined
なので当然といえば当然です。
そこで、クラス全体を 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()
による一般的なモック方法に寄せることが可能です:
export class Klass {
field: string;
constructor() {
console.log("constructor() called");
this.field = "initialzed";
}
}
export function klassBuilder() {
console.log("klassBuilder() called");
return new Klass();
};
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
なお、ソースコードはこちらのリポジトリで公開中です。