テストケースごとにvi.mockでモックした関数の返り値を変えたい
タイトルの通り、テストケースごとに vi.mock
でモックした関数の返り値を変えたいです。
結果的にできたのですが、ググってもAIに聞いてもズバリの情報が得られませんでした。他に良いやり方があるかもしれませんが、とりあえずこれでできましたというメモになります。
前提
src
├── my-module.ts
└── sample.ts
こんな感じで各ファイルがあったとします。
export const getMyValue = async () => {
return 'myValue';
};
import { getMyValue } from './my-module';
export const getMyValueWrapper = async () => {
const myValue = await getMyValue();
return myValue;
};
今回は my-module.ts
の getValue()
を vi.mock
でモックしつつ sample.ts
のテストを書きたいです。
先に完成系
先に完成系を出してしまうと、 vi.fn
と vi.hoisted
を組み合わせるとできました。
import { test } from 'vitest';
import { getMyValueWrapper } from './sample';
const mockGetMyValue = vi.hoisted(() => {
return vi.fn();
});
vi.mock('./my-module', () => {
return {
getMyValue: mockGetMyValue,
};
});
beforeEach(() => {
mockGetMyValue.mockClear();
});
test('foo', async () => {
mockGetMyValue.mockResolvedValue('foo');
expect(await getMyValueWrapper()).toBe('foo');
});
test('bar', async () => {
mockGetMyValue.mockResolvedValue('bar');
expect(await getMyValueWrapper()).toBe('bar');
});
test('baz', async () => {
mockGetMyValue.mockResolvedValue('baz');
expect(await getMyValueWrapper()).toBe('baz');
});
このように書くことでテストケースごとにモックした関数の返り値を変えることができました。
返り値のデフォルトを設定したい場合
返り値のデフォルトを設定したい場合は、 beforeEach
に mockGetMyValue.mockResolvedValue()
を書くと良いです。
beforeEach(() => {
mockGetMyValue.mockClear();
// デフォルト設定
mockGetMyValue.mockResolvedValue('default');
});
test('default', async () => {
// テストケース内で値を設定しなくても `'default'` になる
expect(await getMyValueWrapper()).toBe('default');
});
vi.mockとvi.hoistedと巻き上げについて
vi.mock
は実行時にファイルの先頭に巻き上げられます。
なので次のように書いてもうまくいきません。
import { test } from 'vitest';
import { getMyValueWrapper } from './sample';
test('foo', async () => {
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'foo',
};
});
expect(await getMyValueWrapper()).toBe('foo'); // `actual: 'baz'` になって失敗
});
test('bar', async () => {
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'bar',
};
});
expect(await getMyValueWrapper()).toBe('bar'); // `actual: 'baz'` になって失敗
});
test('baz', async () => {
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'baz',
};
});
expect(await getMyValueWrapper()).toBe('baz'); // これだけ成功する
});
最後のテスト以外は失敗します。なぜでしょうか?
テストファイルのトランスパイル後は次のようになっていると思われます。
// vi.mockはファイルの先頭に巻き上げられる
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'foo',
};
});
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'bar',
};
});
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'baz',
};
});
import { test } from 'vitest';
import { getMyValueWrapper } from './sample';
test('foo', async () => {
expect(await getMyValueWrapper()).toBe('foo');
});
test('bar', async () => {
expect(await getMyValueWrapper()).toBe('bar');
});
test('baz', async () => {
expect(await getMyValueWrapper()).toBe('baz');
});
このため、最後の
vi.mock('./my-module', () => {
return {
getMyValue: async () => 'baz',
};
});
だけが有効となり、すべてのテストで返り値が 'baz'
になってしまいます。
そのため、開発メンバーのコードリーディング時に誤解が起きづらいよう vi.mock
は基本的にファイルの先頭に近い場所かつトップレベルで書くのが良いでしょう。
また、 vi.hoisted
を使うことでファイル先頭に巻き上げて変数を定義できます。
const mockGetMyValue = vi.hoisted(() => {
return vi.fn();
});
vi.mock('./my-module', () => {
return {
getMyValue: mockGetMyValue,
};
});
このようにすると、
// vi.mockより先頭に巻き上げて変数を定義できる
const mockGetMyValue = (() => vi.fn())();
vi.mock('./my-module', () => {
return {
getMyValue: mockGetMyValue,
};
});
のようにトランスパイルされると思われます(そのはず・・・)。
ここまで来れば、テストケースごとに .mockResolvedValue()
で返り値を設定できるようになります(async関数でない場合は .mockReturnValue()
を使う)。
そのほか動かないコードの例
デフォルト値の設定で、最初次のようなコードを書いてうまくいかず、一瞬ハマりました。
const mockGetMyValue = vi.hoisted(() => {
return vi.fn();
});
vi.mock('./my-module', () => {
return {
// デフォルトの返り値を設定?
getMyValue: mockGetMyValue.mockResolvedValue('default'),
};
});
beforeEach(() => {
mockGetMyValue.mockClear();
});
test('default', async () => {
expect(await getMyValueWrapper()).toBe('default'); // `actual: 'undefined'` で失敗
});
直感的にはいけると思ったんですが、返り値が undefined
の関数になってしまうようでした。
console.log(mockGetMyValue.constructor.name); // => `Function`
console.log(mockGetMyValue.mockResolvedValue('default').constructor.name); // => `Function`
.mockResolvedValue()
も Function
インスタンスを返すようなので、いける気がしたんですが 🤔 いずれにせよ元のモックインスタンスを使う必要があります(これ以上深追いはできていません)。
同じハマり方をする人はいないかもしれませんが、一応紹介でした。
以上です。トランスパイル後のコードはドキュメントの記載から想像で書いたので、理解に誤りがあればご指摘ください。
参考
まずは公式。
そしてこちらの記事が大いに理解の手がかりになりました。感謝。
Discussion