Vitest のモック関数 fn、spyOn、mock の使い分け

# はじめに
この記事では、Vitest というテストフレームワークのモックに利用される [```vi.fn```](https://vitest.dev/api/vi.html#vi-fn)、[```vi.spyOn```](https://vitest.dev/api/vi.html#vi-spyon)、[```vi.mock```](https://vitest.dev/api/vi.html#vi-mock) の概要とそれらの使い分けをサンプルつきで記載していきます。

https://vitest.dev/

# ```fn```、```spyOn```、```mock``` の使い分け
モック対象によって使い分けます。

- ```fn```：関数
- ```spyOn```：オブジェクトのメソッド
- ```mock```：モジュール全体

# ```fn```
```fn``` は、関数をモックします。

以下のサンプルでは、```getApples``` というモック関数を作成し、その関数が呼び出されることをテストしています。

```spyFunctions.test.ts
test("spy function no arguments and no returns", () => {
  // Define mock function
  const getApples = vi.fn();
  // call mock function
  getApples();
  // check if mock function is called
  expect(getApples).toHaveBeenCalled();
});
```

また、モック関数に引数や戻り値を与えることもできます。

```spyFunctions.test.ts
test("spy function returns a product", () => {
  const getProduct = vi.fn((product: string) => ({ product }));

  getProduct("apples");

  expect(getProduct).toHaveReturnedWith({ product: "apples" });
});
```

さらに [モック関数向けに提供されている API](https://vitest.dev/api/mock.html) を利用して、モック関数のふるまいを操作することもできます。
例えば、以下のサンプルでは、[mockReturnValueOnce](https://vitest.dev/api/mock.html#mockreturnvalueonce) という API を利用して、モック関数が「5」を返すように操作しています。

```spyFunctions.test.ts
test("spy function returns a number", () => {
  const getApples = vi.fn(() => 0);
  getApples();
  expect(getApples).toHaveReturnedWith(0);

  getApples.mockReturnValueOnce(5);
  getApples();
  expect(getApples).toHaveNthReturnedWith(2, 5);
});
```

# ```spyOn```
```spyOn``` は、引数に指定したオブジェクトのメソッドをモックします。

以下のサンプルでは、```cart``` というオブジェクトの ```getApples``` メソッドをモックし、モックが一度呼び出されることをテストしています。

```spyFunctions.test.ts
const cart = {
  getApples: () => 4,
};

test("spy method", () => {
  // Define mock method
  const spy = vi.spyOn(cart, "getApples");
  // call mock method
  cart.getApples();
  // check if mock is called
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveReturnedWith(4);
});
```

また、[```mockImplementation```](https://vitest.dev/api/mock.html#mockimplementation) を利用して、メソッドを上書きすることもできます。
```spyFunctions.test.ts
const cart = {
  getApples: () => 4,
};

test("overwrite spy method", () => {
  const spy = vi.spyOn(cart, "getApples").mockImplementation(() => 8);
  cart.getApples();
  expect(spy).toHaveReturnedWith(8);
});
```

# ```mock```
```mock``` は、インポートするモジュールを全てモックします。

例えば、以下のように ```calculate.ts``` というモジュールを利用し、```props``` で渡された値の合計を表示するコンポーネントがあるとします。

```calculate.ts
export const sum = (num1: number, num2: number): number => {
  return num1 + num2;
};
```

```Sum.tsx
import { FC } from "react";
import { sum } from "../libs/calculate";

type Props = { num1: number; num2: number };

export const Sum: FC<Props> = ({ num1, num2 }) => {
  const result = sum(num1, num2);
  return <div>The sum is: {result}</div>;
};
```

このコンポーネントを ```mock``` を利用せず、テストすると以下のようなコードになります。

```Sum.test.tsx
test("spy module", () => {
  render(<Sum num1={1} num2={2} />);
  expect(screen.getByText("The sum is: 3")).toBeInTheDocument();
});
```

```mock``` を利用すると、以下のように ```calculate.ts``` の結果を上書きすることができます。
```Sum.test.tsx
test("spy module", () => {
  vi.mock("../../libs/calculate", () => {
    return {
      sum: vitest.fn().mockReturnValue(-10),
    };
  });

  render(<Sum num1={1} num2={2} />);
  expect(screen.getByText("The sum is: -10")).toBeInTheDocument();
});
```

:::note
実際のテストでは、外部モジュールをモックしたいときなどに利用することになると思います。
:::

# 参考
- [expect](https://vitest.dev/api/expect.html)
- [Mock Functions](https://vitest.dev/api/mock.html)
- [Mocking](https://vitest.dev/guide/mocking.html)
- [Vi](https://vitest.dev/api/vi.html)