jestを完全に理解したい（jest.spyOn()編）

## はじめに
jestでメソッドをmockする際、フワッとした知識で使ってたので改めてまとめました。

今回はJest でよく使う以下のmock3兄弟について説明していきます。

1. [jest.fn()](https://qiita.com/only/items/466a09c8602466fe2333)
2. jest.SpyOn()　←今回はこいつについて
3. [jest.mock](https://qiita.com/only/items/07634f917660f2e791b0)

こいつらは似たようなことができるので  
どれを使えばいいのかいつもわからなくなります。  
今回はjest.spyOn()について説明していきます。

## jest.spyOn()
- オブジェクトの特定のメソッドに対してスパイを設定し、そのメソッドの呼び出しを
追跡するための関数です。実際の関数の挙動を保持しつつその関数に対してのスパイが
できるのが特徴です。

#### 1.呼び出しの確認
```typescript:呼び出しを確認
// テスト対象の関数
function greet(name) {
  console.log(`Hello, ${name}!`);
  console.log(`Hi!, ${name}!`);
  console.log(`Ola!, ${name}!`);
  
}


// 関数が呼び出されたかを確認するテスト
test('greet関数が呼び出されたことを確認する', () => {

// 関数のスパイ化
const spyOnGreet = jest.spyOn(console, 'log');

// スパイ化した関数を呼び出す
greet('Bob');

//console.logが呼び出されたかどうかだけを確認
expect(spyOnGreet).toHaveBeenCalled();

//console.logが3回呼び出されたかどうかを確認
expect(spyOnGreet).toHaveBeenCalledTimes(3);

//console.logが1度でも指定した引数で呼び出されているかを確認
expect(spyOnGreet).toHaveBeenCalledWith('Hello, Bob!');

//console.logの最後の呼び出しの引数を確認
expect(spyOnGreet).toHaveBeenLastCalledWith('Ola!, Bob!');

//console.logの1番目の呼び出しの引数を確認
expect(spyOnGreet).toHaveBeenNthCalledWith(1,'Hello, Bob!');

//console.logの2番目の呼び出しの引数を確認
expect(spyOnGreet).toHaveBeenNthCalledWith(2,'Hi!, Bob!');

// スパイを解除
spyOnGreet.mockRestore();
});

```
このようにspyOnを使用するとconsole.logの動作は変えずに
呼び出されたことを確認することができます。
JavaScriptの組み込みオブジェクトやメソッドの呼び出しを監視するときに使います。
__実装自体は変更せずに呼び出しを追跡することができるのがspyOnの優れたところです。__
なので上記のテストでは通常通りconsole.log()は実行されます。


#### 2.呼び出しの戻り値を指定
```typescript:呼び出しの戻り値を指定
function generateRandomNumber() {
  return Math.random();
}

test('generateRandomNumber calls Math.random', () => {
  const spy = jest.spyOn(Math, 'random').mockReturnValue(0.2291245446521175);
  
  const result =  generateRandomNumber();
  
  expect(result).toBe(0.2291245446521175);
  
  spy.mockRestore();
});
```

jest.fn()と同様にmockReturnValueで戻り値を指定することもできます。
もちろん以下のように呼び出された回数ごとに戻り値を指定することもできます。
```typescript:呼び出された回数ごとに戻り値を指定
function generateRandomNumber() {
  return Math.random();
}

test('generateRandomNumber calls Math.random', () => {
  const spy = jest.spyOn(Math,'random')
                   .mockReturnValueOnce(0.2291245446521175)
                   .mockReturnValueOnce(0.2222222222222222)
                   .mockReturnValue(0.1111111111111111)
  
  const result1 =  generateRandomNumber();
  const result2 =  generateRandomNumber();
  const result3 =  generateRandomNumber();
  const result4 =  generateRandomNumber();
  
  expect(result1).toBe(0.2291245446521175);
  expect(result2).toBe(0.2222222222222222);
  expect(result3).toBe(0.1111111111111111);
  expect(result4).toBe(0.1111111111111111);
  
  spy.mockRestore();
});
```
mockReturnValueOnceで指定したい回数分戻り値を指定してmockReturnValueで
デフォルトの返り値を指定します。なので3回目の呼び出し以降は0.1111111111111111が
戻るようになります。


#### 3.呼び出しの実装を指定する
```typescript:呼び出しの実装を指定する
async function fetchUserGreting(userId) {
  const response = await fetch(`/api/user/${userId}`)
  const user = await response.json()
  return `ユーザーID:${user.id}の${user.name}さん、こんにちは！`
}

test('fetchの実装を変更', async () => {
 
  const mockFetch =
  jest.spyOn(global,'fetch').mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve({ id: 1, name: 'Ken' }),
    })
  )

  const result = await fetchUserGreting(1)

  expect(result).toBe(ユーザーID:1のKenさん、こんにちは！)
  mockFetch.mockRestore();
});
```
このように外部APIへのリクエストを行う関数をテストする場合、実際にAPIへリクエストを送るとテストの速度が遅くなったり、ネットワークの状況によりテストの結果が変わってしまったりする可能性があるのでこのように実際のリクエストを送らずに
mockImplementationでfetchメソッドの実装を指定して安定したテストを行うことができます。

#### mockのリセット忘れずに！
spyOnを使用した際、他のテストメソッドに影響を与えないように
mockRestore()を使って元の実装に戻してあげましょう。
複数のspyOnの実装を一気に戻したいときは
```
jest.restoreAllMocks()
```
で全て戻しちゃっても良いと思います。
なんならbeforeEachに書いちゃっても良いと思います。


## 終わりに
ご指摘等ありましたらコメントください。