JestのMock化パターン4本ノック

はじめまして、インターンのmasuyamaと申します。rexcornuではフロントエンド開発を担当しています。

今まで業務経験がなかったため、こうして単体テストのコードを書くのは初めての挑戦です。

今回は、単体テストを書く上で学んだことを共有するために、主にJestのMockと呼ばれる機能に焦点を当てて記事を書いてみたいと思います。

（〜本ノックと題していますがクイズ系の記事ではありません）

# はじめに：

そもそも「Jestとは？」「Mockとは？」と思う方もいるはずなので、簡単に用語および開発背景の説明を行います。

 ###  Jestとは？

JestはMeta社が開発したJavaScriptテスティングフレームワークです。

### Mockとは？

Mockとは、テスト対象のコードが依存する外部のシステムやオブジェクトを模倣するための機能です。

例えば、データベースや外部APIなど、実際にはテスト実行時にアクセスしたくない外部依存性がある場合に、これらを模擬的に再現したオブジェクトを使用します。

つまり、Mock化の主な目的は、テスト対象が持つ外部依存性を排除することにあります。

# 単体テストの目的

単体テストは、プログラムの個々のモジュール（関数、メソッド、コンポーネント）が正しく動作するかどうかを検証するためのテストです。

テスト対象となるモジュールが他のモジュールや外部システムに依存している場合、その依存関係を排除してテストを行う必要があります。

今回の記事では、JestのMock機能を使って、テスト対象のコードが依存する外部のシステムやオブジェクトを模倣する方法をいくつか紹介します。

## Mockパターン

それでは、僕が実際に書いたMock化のコードを、より一般的な例に移し替えたうえで、4つほど紹介していきたいと思います。

### 1. window.ResizeObserverのMock化

window.ResizeObserverとは、DOM要素のサイズ変更を監視するためのAPIです

今回はブラウザでの環境を想定していないため、window.ResizeObserverをそのまま使うことはできません。

そのため、window.ResizeObserverをMock化して、テスト実行時にエラーが発生しないようにする必要があります。

下のテストコードではResizeObserverのコンストラクタをjest.fn()でMock化し、空のobserveメソッドとdisconnectメソッドを持つオブジェクトを返すようにしています。

#### テストコード
```javascript
window.ResizeObserver = jest.fn().mockImplementation(() => ({
    disconnect: jest.fn(),
    observe: jest.fn(),  
}));
```

### 2. axiosのget通信のMock化

例えば、以下のようなaxiosのget通信があるとします。

このコードをテストするためには、axiosのget通信をMock化する必要があります。

あらかじめget通信のレスポンスのダミーをjsonファイルとして用意しておき、axios.getの戻り値をPromise.resolveで返すようにします。

 #### テスト対象のコード
 ```javascript

import axios from 'axios';
axios.get("user_list").then((res) => {
    console.log(res.data);
}).catch((err) => {
    console.log(err);
});
axios.get("product_list").then((res) => {
    console.log(res.data);
}).catch((err) => {
    console.log(err);
});
```


#### テストコード
```javascript
const dummyUserList = require(./mock/dummyUserList.json)
const dummyProductList = require(./mock/dummyProductList.json)
jest.mock("./axios");
axios.get = jest.fn();
axios.get.mockImplementation((url) => {
    if (url === "user_list") {
        return Promise.resolve({ data: dummyUserList });
    }
    if (url === "product_list") {
        return Promise.resolve({ data: dummyProductList });
    } 
    return Promise.resolve({data:{}})
});
```
 ### 3. localStorageのMock化

下のようにlocalStorageを使ってデータを保存している場合、localStorageをMock化してテストを行う必要があります。

localStorage.getItemメソッドのMock化には、jest.spyOnを使用します。

jest.spyOnは、オブジェクトのメソッドをMock化するための関数です。

localStorage.getItemメソッドをMock化することで、テスト実行時にlocalStorage.getItemメソッドが呼び出された場合に、指定した値を返すようにします。

 #### テスト対象のコード
```javascript
const basket = localStorage.getItem('basket');
const user = localStorage.getItem('user');
```
#### テストコード
```javascript
const dummyBasket = require(./mock/dummyBasket.json)
jest.spyOn(Storage.prototype, "getItem").mockImplementation((key) => {
    if (key === 'basket') {
        return dummyBasket;
    }
	if(key == 'user') {
	    return “testUser”
    }
    return 0;
});
```
### 4. useContextのmock化

外部のContextを扱うコンポーネントをテストする場合、そのContextを提供するモジュールごとMock化する必要があります。

ここでは、Child1, Child2コンポーネントがParentコンポーネントから提供されるContextを使用しているとします。

ParentコンポーネントのContextをMock化するために、jest.mockを使用し、Parentコンポーネント全体をMock化します。

こうすることで、ParentContextが提供する値をMock化することができます。

#### テスト対象のコード
#####  Parent.jsx
```javascript
export const ParentContext = React.createContext();

const Parent = () => {
    const value1 = "value1";
    const value2 = "value2";
    return (
        <ParentContext.Provider value={{ value1, value2 }}>
            <Child1 />
            <Child2 />
        </ParentContext.Provider>
    );
}
export default Parent;
```
#####  Child1.jsx
```javascript
import { ParentContext } from './Parent';
const Child = () => {
    const { value1 } = useContext(ParentContext);
    return <div>{value}</div>;
}
```

##### Child2.jsx
```javascript
import { ParentContext } from './Parent';
const Child = () => {
    const { value2 } = useContext(ParentContext);
    return <div>{value}</div>;
}
```
#### テストコード
```javascript
import { ParentContext } from './Parent';
jest.mock('./Parent', () => {
    const originalModule = jest.requireActual('./Parent');
    return {
        __esModule: true,
        ...originalModule,
        ParentContext: {
            _currentValue: { value1: 'mockValue1', value2: 'mockValue2' },
        },
    };
});
```
# 今後の課題

ContextのMock化の例では、親コンポーネントを丸ごとMock化しているため、Contextが提供する値が多くなればなるほど前もって用意しなければならないダミーデータが増え、テストコードが複雑になるという問題がありました。

さらに効率的な方法を模索するために、React hooksおよび単体テスト手法について学習を進めていきたいと思います。

# おわりに

rexcornuでは、これからの会社の成長を共にしてくれる仲間を募集しています。

https://www.rexcornu.com/career

https://www.wantedly.com/companies/company_9295588/projects

https://www.rexcornu.com/

https://c.tech-match.net/