最近vitestを使った単体テストを実装する機会が増えたが、fn()などをなんとなくでしか使えていないので、ここらで整理しておく。
vi.fn()
モック関数を生成する。
const hogeFunction = vi.fn();
モック関数を生成することで、以下のこと等を記録・確認することができる
- モック関数が呼び出された回数(
toHaveBennCalled()
) - モック関数に渡された引数の内容(
toHaveBeenCalledWith()
) - モック関数が返した値(
toHaveReturnedWith()
)
モック関数には任意の返り値を設定することができる。
const hogeFunction = vi.fn().mockReturnValue('Hello');
// →hogeFunction()を呼び出すと'Hello'が出力されるようになる
// 一度モック関数を作成した後に、返り値を設定することもできる
const hogeFunction = vi.fn();
hogeFunction.mockReturnValue('Hello'); // 常に'Hello'を返す
hogeFunction.mockImplementation(() => 100); // モックが呼び出されたときに実行される関数を定義できる
// 呼び出した時の結果は、後から定義したものに上書きされる
vi.fn()を使ったテスト例
it('vi.fn()を使ったテストの例', async () => {
const hogeFunction = vi.fn().mockReturnValue('Hello');
// targetはテストしたい関数。第一引数に関数を受け取ると想定
const result = await target(hogeFunction);
expect(result).toBe('hogehoge')
})
vi.fn()はモック関数を新たに作成している。元の関数を上書きするわけではない。
vi.spyOn()
オブジェクトのメソッドをメソッドを監視し、必要ならばモック化することができる関数。
const cart = {
getFruits: () => 40
};
const spy = vi.spyOn(cart, 'getFruits'); // getApplesを監視
監視することによって、対象(ここではgetApples)が何回呼び出されたのか、そのときにどのような引数を渡されたのかを確認することができる。
const cart = {
getFruits: (fruits) => fruits === 'Apple' ? 10 : 0;
};
test('getFruits呼び出しについてのテスト', () => {
const spy = vi.spyOn(cart, 'getFruits'); // getApplesを監視
const result = cart.getFruits('Apple');
// resultの値をテスト
expect(result).toEqual(40);
// spyから、getFruitsの呼び出しについてテスト
expect(spy).toHaveBeenCalledTimes(1); // 1回呼び出し
expect(spy).toHaveBeenCalledWith('Apple'); // 呼び出されるとき引数としてAppleを渡された
})
また、監視するだけではなく、必要に応じてモック化することもできる。
const cart = {
getFruits: () => 40
};
const spy = vi.spyOn(cart, 'getFruits').mockImplementation(() => 20);
クラスをモックする、という使い方もある。
export class Calculator {
add(a, b) {
return a + b;
}
}
import * as Calculator from './Calculator';
const spy = vi.spyOn(Calculator.prototype, 'add')mockImplementation(
(a, b) => a + b + 3
);
const calc = new Calculator();
expect(calc.add(1, 2)).toEqual(6);
なぜspyOn(Calculator, 'add')
ではなくspyOn(Calculator.prototype, 'add')
なのか
JavaScriptの仕様。
クラス内でどのように関数を定義しているかによって、どのようにモックすべきかが異なる。これは、定義の書き方によって、「関数がどこに載っているのか」が異なるためである。
class Svc {
// prototype
calc(x: number) { return x * 2; }
// static
static version() { return "1.0.0"; }
// instance field (arrow)
run = (x: number) => x + 1;
}
上記の関数を例に見ていく。
calc()はprototypeに載る。 そういう仕様と認識して良い。
一般的なコードとして使用するときは、わざわざprototypeを書く必要はないが、モックするときは指定する必要がある。
test("prototype メソッドを spy", () => {
// モック作成時にprototypeを明記する必要がある
vi.spyOn(Svc.prototype, "calc").mockReturnValue(999);
// 処理として呼び出すときには、prototypeを明記する必要はない
expect(new Svc().calc(3)).toBe(999);
});
static version()はクラス本体に載る。
test("static メソッドを spy", () => {
vi.spyOn(Svc, "version").mockReturnValue("MOCK");
expect(Svc.version()).toBe("MOCK");
});
アロー関数であるrun()はインスタンス自身に載る。 なので、モックするときは一度インスタンスを作成する必要がある。
test("矢印関数はインスタンスに対して spy", () => {
const s = new Svc();
vi.spyOn(s, "run").mockReturnValue(42);
expect(s.run(5)).toBe(42);
});
また、ファイルごとインポートして、そこで定義されている関数をモック化するのにspyOnを使うこともできる。
// func.ts
export const add = (a, b) => {
return a + b;
}
//index.test.ts
import * as Functions from './func.ts';
vi.spyOn(Functions, 'add').mockImplementation(() => a + b + 3);
expect(sub(1, 2)).toBe(6);
spyOnでモック化すると、元の関数が上書きされる。他のテストに影響を及ぼさないようにするため、元に戻すのが一般的。
import { afterEach, vi } from "vitest";
afterEach(() => vi.restoreAllMocks());
vi.mock()
モジュールごと丸ごとモックするときに使う。
// math.ts
export function add(a: number, b: number) {
return a + b;
}
export function sub(a: number, b: number) {
return a - b;
}
// index.test.ts
vi.mock('./math.ts', () => {
return {
add: vi.fn().mockReturnValue(10),
sub: vi.fn()
}
})
test('addが1度呼び出されたことをテスト', () => {
add(1, 2);
expect(vi.mocked(add)).toHaveBeenCalledTimes(1);
expect(vi.mocked(add)).toHaveBeenCalledWith(1, 2);
})
インポートしたファイルで定義されている関数は、基本的にvi.fn()で上書きされる。もし関数ごとの振る舞いを明示していない場合、関数はundefinedを返す。
vi.mock()はモジュールがインポートされる前に実行される。つまり、本来の実装を読み込むことがなくなる。そのため、restoreAllMocks()を実行しても意味がない。
使い分け
vi.fn()
- モック関数を新たに作成したいとき。
vi.spyOn()
- 既存のメソッドの機能はそのままに、何回呼び出されたのかなどを監視したいとき。
- クラスメソッドをモックしたいとき。
vi.mock()
- モジュール丸ごとモックしたいとき。