【Jest】モック化はこれでOK！

# Jestを利用してモック化しよう

テストを作成していて、時刻によって返す値が異なる関数などのテストを書くとき、想定した値を返してもらいたいときがあります。```jest.fn()```を利用すると簡単に関数をモック化する事ができます。 この記事は、学習した際の自分用の備忘録です。

https://jestjs.io/ja/docs/mock-functions

## mockプロパティの確認
すべてのモック関数には、```.mock```プロパティがあります。モック関数呼び出し時のデータと、関数の返り値が記録されています。はじめにmockプロパティを確認していきます。

* calls : モック関数の呼び出し毎の引数の配列
* results: モック関数の呼び出し毎の結果の配列
* instances : newを使用してモック関数からインスタンス化されたオブジェクトが含まれる配列

```javascript:mock.test.js
describe("#jest.fn", () => {
  it("Check `jest.fn()` specification", () => {
    const mockFunction = jest.fn();
    expect(mockFunction("test")).toBe(undefined); // mockFunction関数の結果は`undefined`である

    expect(mockFunction).toHaveProperty("mock"); // mockFunction関数はmockプロパティを持っている

    expect(mockFunction.mock.calls.length).toBe(1); // mockFunction関数は1度呼び出された

    expect(mockFunction.mock.calls[0]).toEqual(["test"]); // mockFunction関数が1度呼び出された際に、引数は"test"だった

    expect(mockFunction.mock.results.length).toBe(1); // mockFunction関数の結果は1つある

    expect(mockFunction.mock.results[0].type).toBe("return"); // mockFunction関数が1度目に呼び出された結果は正常にリターンされている

    expect(mockFunction.mock.results[0].value).toBe(undefined); // mockFunction関数の1度目の結果は`undefined`である

    expect(mockFunction.mock.instances[0]).toBe(undefined); // mockFunction関数からnewを利用してインスタンスを作成していない
  });
});
```

上記の検証結果から、単純にjest.fn()でモック関数を作成した場合、```undefind```が返り値として設定されることがわかります。

## mockの戻り値を変更しよう

モック関数の戻り値を変更するには、```mockImplementation```を利用します。

```javascript:mock.test.js
 it("return `Hoge`", () => {
    const mockFunction = jest.fn().mockImplementation(() => "Hoge"); // mockFunction関数の返り値にHogeを設定
    expect(mockFunction()).toBe("Hoge");
  });
```

省略することも可能です。

```javascript:mock.test.js
 it("return `Hoge`", () => {
    const mockFunction = jest.fn(() => "Hoge");
    expect(mockFunction()).toBe("Hoge");
  });
```

## 関数の呼び出し毎にテストしよう

モック関数の呼び出し毎の結果のテストを確認したい場合、```mockImplementationOnce```関数を利用します。デフォルトは```undefined```が返されます。

```javascript:mock.test.js
 it("return true once then it returns false", () => {
    const mockFunction = jest
      .fn()
      .mockImplementationOnce(() => true)
      .mockImplementationOnce(() => false);
    expect(mockFunction()).toBe(true);
    expect(mockFunction()).toBe(false);
    expect(mockFunction()).toBe(undefined); // デフォルトの返り値である`undefined`がリターンされる
  });
});
```

上記のテストでは、mockImplementationOnceを1回目はtrueを返すように設定して、２回目はfalseを返すように設定しています。
３回目は何も設定していないので、``undefined```が返されます。


## 特定の関数をモック化しよう

```jest.spyOn()```を使用することで、オブジェクトの特定の関数をモック化することができます。 さらに、```jest.spyOn()```でモック化した場合は、```mockRestore```を実行することで、オリジナルの関数へ戻すことができます。

```javascript:mock.test.js
describe("#spyOn", () => {
  const spy = jest.spyOn(Math, "random").mockImplementation(() => 0.1); // Math.random()は0.1を返す、オリジナルの関数では0から1以下を返します

  afterEach(() => {
    spy.mockRestore();
    // jest.restoreAllMocks(); // 他にモック化している関数があれば、こちら1行ですべてのモック化した関数を元に戻すことができます
  });

  it("Math.random return 1", () => {
    expect(Math.random()).toEqual(0.1);
  });

  //afterEachが実行され、randomは0から1以下を返す

  it("Math.random return under 1", () => {
    expect(Math.random()).toBeLessThan(1); // 1未満である
    expect(Math.random() < 1).toEqual(true); // toEqualで1未満であることを評価する
  });
});
```

## ここまでのおさらい

下記のコードのように時間によって、返す値が異なる関数があります。そこで、モックで時刻を作成し、テストを作成します。

```javascript:greeter.js
const capitalize = (str) => {
  return str.slice(0, 1).toUpperCase() + str.slice(1, str.length);
};

export const greet = (name) => {
  const capitalizedName = capitalize(name);
  const hour = new Date().getHours();
  const greetMessage = hour >= 6 && hour < 12 ? "Good morning" : "Hello";
  return `${greetMessage} ${capitalizedName}!`;
};
```

```javascript:greeter.test.js
import greeter, { greet } from "./greeter";

describe("#greeter", () => {
  describe("#greet", () => {
    const noonTime = new Date("2020-10-10T15:00:00");
    const morningTime = new Date("2020-10-10T08:00:00");

    beforeEach(() => {
      Date = jest.fn(() => noonTime);
    });

    describe("mock date function", () => {
      it("Hello <name> when the time is 12:00 - 05:59", () => {
        expect(greet("hoge")).toEqual("Hello Hoge!");
      });

      it("Good morning <name> when the time is 06:00-11:59", () => {
        Date = jest.fn(() => morningTime);
        expect(greet("foo")).toEqual("Good morning Foo!");
      });
    });
  });
});
```


## 外部モジュールのモック化

外部モジュールのaxiosを使った下記のコードのテストコードを書きたいと思います。テストコードでは、実際にAPIを叩かずにaxiosの箇所をモック化していていきます。

```javascript:users.js
import axios from "axios";

class Users {
  static all() {
    return axios.get("/users.json").then((resp) => resp.data);
  }
}

export default Users;
```

外部モジュールをモック化する際は、jest.mockを利用します。第１引数にモジュール名を設定することで、モジュール全体をモック化することができます。下記のコードでは、axiosを```jest.mock("axios");```と記載してモック化しています。モック化したモジュールに対して、```mockResolvedValue```と```mockImplementation```を利用すると返り値を設定することができます。

* モジュールのモック化
    * jest.mock("axios");

* 返り値を設定
    * axios.get.mockResolvedValue(resp);
    * axios.get.mockImplementation(() => Promise.resolve(resp))


```javascript:users.test.js
import axios from "axios";
import Users from "./users";

jest.mock("axios");

test("should fetch users", async () => {
  const users = [{ name: "Bob" }];
  const resp = { data: users };

  axios.get.mockResolvedValue(resp);
  //axios.get.mockImplementation(() => Promise.resolve(resp))

  await expect(Users.all()).resolves.toEqual(users);
});
```

https://jestjs.io/ja/docs/mock-functions#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%A2%E3%83%83%E3%82%AF

## モックのリセット

* ```mockFn.mockClear()```:mockのプロパティをすべてリセットする
* ```mockFn.mockReset()```: mock のプロパティをすべてリセットする、設定したmock関数をクリアする 
    * オリジナルの関数になるわけではない
* ```mockFn.mockRestore()``` :mock関数をオリジナルの関数へ戻す
    * spyOn を利⽤して、モック化した関数のみ有効

下記の関数は、すべてのモックに対して効果があります

* ```jest.clearAllMocks()``` : すべてのmockのプロパティをすべてリセットする
* ```jest.resetAllMocks()``` : すべての mock のプロパティをすべてリセットする、設定したmock関数をクリアする
    * オリジナルの関数になるわけではない
* ```jest.restoreAllMocks()``` : すべてのmock関数をオリジナルの関数へ戻す
    * spyOnを利⽤して、モック化した関数のみ有効



https://jestjs.io/ja/docs/jest-object#jestclearallmocks


spyOnとjest.fnは、```jest.restoreAllMocks()```を実行した際に、spyOnは、モック化したDate関数がオリジナルに戻る
という違いがある。


```javascript:mock.test.js
describe("#reset mocks with spyOn", () => {
  const mockDate = new Date("2019-01-01");
  const originalDate = new Date("2020-12-25");
  let spy = null;

  beforeEach(() => {
    //  固定した値を返すようにする。
    spy = jest.spyOn(global, "Date").mockImplementation(() => mockDate);
  });

  afterEach(() => {
    spy.mockRestore();
  });

  it("jest.clearAllMocks", () => {
    // Dateに引数で他の日時を与えても、mockDateが返される
    expect(new Date("2020-02-14")).toEqual(mockDate);

    // 引数の確認
    expect(spy.mock.calls).toEqual([["2020-02-14"]]);

    // mock関数の返り値がオブジェクトである場合、mockInstanceは作成されない。なぜそうなるのかJestのissue→https://github.com/facebook/jest/issues/10965
    expect(spy.mock.instances).toEqual([{}]);

    // 結果の確認
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // すべてのmockのプロパティをすべてリセットする
    jest.clearAllMocks();

    // mockのプロパティがすべてリセットされているか確認
    expect(spy.mock.calls).toEqual([]);
    expect(spy.mock.instances).toEqual([]);
    expect(spy.mock.results).toEqual([]);

    // mock関数は引き続き利用できる
    expect(new Date("2020-12-25")).toEqual(mockDate);
  });

  it("jest.resetAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(spy.mock.calls).toEqual([["2020-12-25"]]);
    expect(spy.mock.instances).toEqual([{}]);
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    //すべての mock のプロパティをすべてリセットする、設定したmock関数をクリアする
    jest.resetAllMocks();

    // mockのプロパティがすべてリセットされる
    expect(spy.mock.calls).toEqual([]);
    expect(spy.mock.instances).toEqual([]);
    expect(spy.mock.results).toEqual([]);

    // mock関数はリセットされ、デフォルトでは`{}`が返される
    expect(new Date("2020-12-25")).toEqual({});
  });

  it("jest.restoreAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(spy.mock.calls).toEqual([["2020-12-25"]]);
    expect(spy.mock.instances).toEqual([{}]);
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    //すべてのmock関数をオリジナルの関数へ戻す
    jest.restoreAllMocks();

    // mockのプロパティはリセットされない
    expect(spy.mock.calls).toEqual([["2020-12-25"]]);
    expect(spy.mock.instances).toEqual([{}]);
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // mock関数がリセットされ、オリジナルのDate関数が実行される
    expect(new Date("2020-12-25")).toEqual(originalDate);
  });
});

describe("#reset mocks with jest.fn", () => {
  const mockDate = new Date("2019-12-21"); // 1年前の今日
  const originalDate = new Date("2020-12-25");

  beforeEach(() => {
    Date = jest.fn(() => mockDate);
  });

  it("jest.clearAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // リセット
    jest.clearAllMocks();

    // mockのプロパティがすべてリセットされる
    expect(Date.mock.calls).toEqual([]);
    expect(Date.mock.instances).toEqual([]);
    expect(Date.mock.results).toEqual([]);

    // mock関数は引き続き利用できる
    expect(new Date("2020-12-25")).toEqual(mockDate);
  });

  it("jest.resetAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    jest.resetAllMocks();

    // mockのプロパティがすべてリセットされる
    expect(Date.mock.calls).toEqual([]);
    expect(Date.mock.instances).toEqual([]);
    expect(Date.mock.results).toEqual([]);

    // mock関数はリセットされ、デフォルトでは`{}`が返される
    expect(new Date("2020-12-25")).toEqual({});
  });

  it("jest.restoreAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    jest.restoreAllMocks();

    // mockのプロパティはリセットされない
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // spyOnの場合と異なり、jest.fnで関数にモック関数を上書きした場合は、restoreAllMocksを利用してもオリジナルの関数へは元に戻らない
    expect(new Date("2020-12-25")).not.toEqual(originalDate);
    expect(new Date("2020-12-25")).toEqual(mockDate);
  });
});
```