なぜJestのmockライブラリに混乱してしまうのか？

# はじめに

JavaScriptのモックライブラリでは、 `sinon` などが有名であるが、テスティングフレームワークに `Jest` を使ってるならば Jest組み込みのモックライブラリで済ませたほうが学習コスト少なくて済むだろうと思える。

しかし、 `sinon` の感覚でJestのモックライブラリを使おうとすると違和感というのか、モックへの考え方の違いに気づかされる。 ということで今回は、Jestのモックライブラリの考え方と使い方を整理していきたいと思う。

# モックの用語整理とJestモックライブラリの位置づけ

モックと一言でいっても、それが指す内容は微妙に異なる。 ここでは、モックを `広義のMock Object` と `狭義のMock Object` と分けて整理してくれている[テスト駆動開発](https://www.amazon.co.jp/dp/B077D2L69C/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)を参考に用語を整理する。

テスト駆動開発では、モック用語を、下図のとおり、テストダブルとそのサブクラスとして `Dummy, Spy, Stub, Mock, Fake` という分け方をする。 これはxUnitを活用したテストコードの形式・パターンを整理する中で登場した考え方であり、 このテストダブルを、 `広義のMock Object` と呼び、サブクラスのmockは、 `狭義のMock Object` として両者を明確にわける。 よく耳にするモックモックというのは、 `広義のMock Object` を指しているということになる。

<img width="500px" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157917/d6953b18-320e-458b-be99-2f9eacba57fe.jpeg">

sinonはこのxUnitの分類に沿って作られている。
よって、 `spy`, `stub`, `fake` , `mock` を明確に分けてAPIを提供している。

一方でJestの場合、どちらかいうと `広義のモック` という観点でAPIを提供してる。
このため spy, stubメソッドはどれかなどとAPIを探しては混乱してしまうことになる。

spyやstubをJestのモックライブラリで使いこなすにはどのように実装すればよいのかということと紹介していこうと思う。

# モックする前の設計整理

spy, stubを実装していく前に、まずどのような関数をspy, stubさせていくのかを説明する。
今回モック対象として関数を利用するが、オブジェクト・クラスであっても同じである。

まず以下の２つの関数を用意する。

```typescript:src/double.ts
function double(x: number): number {
  return 2 * x;
}
```

`double`は受け取った引数を2倍にして返す関数である。

```typescript:src/doubleSquare.ts
function doubleSquare(x: number): number {
  const d = double(x);
  return d * d;
}
```

`doubleSquare` は、引数を2倍にして自乗する関数である。 2倍にするときに、 `double` 関数を利用している。

これら関数の関係をシーケンス図にすると次のような関係になっている。

![jest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157917/db87cf90-c380-1772-7250-a7c03810f4b7.png)


上図のとおり、doubleSquareはdoubleを利用している、すなわちdoubleに依存しているわけだ。 よって、 `doubleSquare` 関数をテストするならば、この依存をうまく検出・排除していくことになる。 プロダクションコードの場合、doubleの部分がHTTPリクエストやDBアクセスなどにとって変わると理解してもらえるよい。

# spy

spyの役割は、その名の通りスパイのように特定の関数に忍び込み、その関数のinput/outputがなんであるか情報を盗み取ることである。 引数に何が渡ってきたのか・どんな値を返したのか・返さなかったのかを記録してくれる。 そしてStubとは異なり振る舞い自体を変更することはしない。

今回の場合では、上図の(1)のinput/outputを盗聴し、(2)の結果を変更したりしないということになる。

これをJestで実装していくが、JestはxUnitで分類したようにSpy, Stubなどを明確に使い分けしない。 それより `広義のmock` という枠組みで整理されているので、spyの実装も、 `spy` 関数や, `mock` 関数の両方が登場してくる。


## 依存先がexport defaultを使っている場合

double関数を `export default` を使って、他からインポート可能にする。

```typescript:src/double.ts
export default function double(x: number): number {
  return 2 * x;
}
```

doubleSquareでは、doubleを`import`する。

```typescript:src/doubleSquare.ts
import double from './double'

export default function doubleSquare(x: number): number {
  const d = double(x);
  return d * d;
}
```

これで対象ファイルが用意できたので、テストコードを作成していく。

### jest.mockをつかう

まずひとつめのspyのやり方は、jest.mockを使うやり方になる。
mockだけどspy機能をもってるのが、説明してきたJestのモック定義の捉え方である。

```typescript
import * as assert from 'power-assert';
import double from '../../src/mock/export-default/double';
import doubleSquare from '../../src/mock/export-default/doubleSquare';

jest.mock('../../src/mock/export-default/double', () => {
  const originalModule = jest.requireActual(
    '../../src/mock/export-default/double'
  );
  return {
    __esModule: true,
    default: jest.fn().mockImplementation(originalModule.default),
  };
});

describe('spy export default function with jest.mock', () => {
  afterEach(() => {
    // doubleはmockされていても、importしたときの情報しかもたないので、
    // mockメソッドを使用するには、jest.Mockにキャストが必要
    (double as jest.Mock).mockClear();
  });

  it('doubleが呼び出されるのは1回', () => {
    const actual = doubleSquare(3);
    // .mock.callsは呼び出した回数分の引数の情報がはいる
    // lengthを使えば、何回呼び出されたのかがわかる
    assert.equal((double as jest.Mock).mock.calls.length, 1);
  });

  it('doubleの引数は3である', () => {
    const actual = doubleSquare(3);
    // .mock.calls[index]でindex回目の引数情報にアクセスできる
    assert.equal((double as jest.Mock).mock.calls[0], 3);
  });

  it('double処理は正常終了し、例外をなげない', () => {
    const actual = doubleSquare(3);
    // .mock.resultsは、mock対象の出力情報を配列管理している
    // [0]ではじめに呼び出されたときの出力情報にアクセスでき, typeでそれが正常終了, 異常終了などを判定できる
    assert.equal((double as jest.Mock).mock.results[0].type, 'return');
  });

  it('doubleのreturn値は3を2倍した6である', () => {
    const actual = doubleSquare(3);
    // .mock.results[index].valueでindex回目に出力した値にアクセスできる
    assert.equal((double as jest.Mock).mock.results[0].value, 6);
  });

  // spyなのでdouble関数の振る舞いはそのまま反映されることを確認
  it('doubleSquareの戻り値は、(2 * 3) ^ 2 で36になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 36);
  });
});
```

この書き方は、mockしたときに振る舞いをもともとの関数で上書きすることで、spyにしてしまうというやり方である。 Jestのmockインスタンスは、spyの機能を持っている。 `.mock` を使うとその入出力を調べることができる。 だが、mockするとstubのように返り値も変更してしまうので、元々の関数で振る舞いを上書きしようというわけだ。

`jest.mock()` の第一引数はモックしたいモジュールのファイルパスをさし、第二引数は、そのモジュールの振る舞いを定義する関数である。 この場合、まず `'../src/double'` をモックし、returnされるオブジェクトに振る舞いを変更している。

もとの関数の振る舞いを取得したいので、 `jest.requireActual` を使い、 `jest.fn().mockImplementation(originalmodule.default)` で同じ振る舞いさせている。 
`export default` の関数をモックするときに注意すべきは、 `__exModule: true`, `default` というキーである。 
これが絶対必要になる。

### jest.spyOnをつかう

また別の書き方として、 `jest.spyOn` を使ってspyを実現できる。

```typescript
import doubleSquare from '../src/doubleSquare';

describe('spy export default', () => {
  let spy: jest.SpyInstance;

  beforeAll(() => {
    const org = require('../src/double');
    spy = jest.spyOn(org, 'default');
  });

  afterEach(() => {
    // 毎回テストごとに盗聴した入出力情報をリセットする
    spy.mockClear();
  });

  it('呼び出されるのは1回', () => {
    const actual = doubleSquare(3);
    // spy.mock.callsは呼び出した回数分の引数の情報がはいる
    // lengthを使えば、何回呼び出されたのかがわかる
    assert.equal(spy.mock.calls.length, 1);
  });

  it('引数は、3である', () => {
    const actual = doubleSquare(3);
    // spy.mock.calls[index]でindex回目の引数情報にアクセスできる
    assert.equal(spy.mock.calls[0], 3);
  });

  it('処理は正常終了し、例外をなげない', () => {
    const actual = doubleSquare(3);
    // spy.mock.resultsは、spy対象の出力情報を配列管理している
    // [0]ではじめに呼び出されたときの出力情報にアクセスでき, typeでそれが正常終了, 異常終了などを判定できる
    assert.equal(spy.mock.results[0].type, 'return');
  });

  it('return値は3である', () => {
    const actual = doubleSquare(3);
    // spy.mock.results[index].valueでindex回目に出力した値にアクセスできる
    assert.equal(spy.mock.results[0].value, 3);
  });

  it('doubleSquareの戻り値は、(2 * 3) ^ 2 で36になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 36);
  });
});
```

spyインスタンスから、 `.mock` にアクセスすることで、入出力を調査することができる。

`spyOn` メソッドは必ず第二引数を求められるため、次のように `export default` 関数を `import` して利用することはできない。

```typescript
import double from '../src/double';
jest.spyOn(double, 'default')
```

代わりに `require` を使うと、 `export` された関数は `default` というkey名でアクセスできるので、 `beofreAll` 内で`jest.spyOn(org, 'default')` と記述し、spyを可能にしている。

どうしても `import` を使いたい場合、 `alias` を使えば次のようにもかける。

```typescript
// * asを利用することでdefaultでアクセス可能にする
import * as double from '../src/double';

describe('spy export default', () => {
  let spy: jest.SpyInstance;
  beforeAll(() => {
    spy = jest.spyOn(double, 'default');
  });

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

  it('spyが呼び出されるのは1回', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.calls.length, 1);
  });

  it('spyの引数は、3である', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.calls[0], 3);
  });

  it('spyは正常に処理が終了する', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.results[0].type, 'return');
  });

  it('spyのreturn値は3である', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.results[0].value, 3);
  });

  it('doubleSquareの戻り値は、(2 * 3) ^ 2 で36になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 36);
  });
});
```

## 依存先がexportを使っている場合

export defaultではなく、 `export` を利用するので、もとのコードを以下のように変更する。

```typescript:src/double.ts
export function double(x: number): number {
  return 2 * x;
}
```

### jest.mockをつかう

```typescript
import { double } from '../src/double';

jest.mock('../src/double').mockimplementation(() => {
  const originalModule = jest.requireActual('../src/double');
  return {
    double: jest.fn().mockImplementation(originalModule.double),
  }
});
```

詳細実装は割愛するが、基本export defaultのときと同じである。 
違いは、 `__esModule: true`, `default` がなくなっていることである。


### jest.spyOnをつかう

`require` を使用するが、export defaultのときと同じになる。

```typescript
import doubleSquare from '../src/doubleSquare';

describe('spy export', () => {
  let spy: jest.SpyInstance;

  beforeAll(() => {
    const org = require('../src/double');
    spy = jest.spyOn(org, 'double');
  });

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

  it('呼び出されるのは1回', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.calls.length, 1);
  });

  it('引数は、3である', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.calls[0], 3);
  });

  it('処理は正常終了し、例外をなげない', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.results[0].type, 'return');
  });

  it('return値は3である', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.results[0].value, 3);
  });

  it('doubleの戻り値は、36になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 36);
  });
});
```

aliasを使用する場合

```typescript
import * as myModule from '../src/double';
const spy = jest.spyOn(myModule, 'double')
```



# stub

Jestのstubの機能についてみていく。stubの役割は出力情報を書き換えることにある。 たとえば、DBアクセス、HTTP通信などの副作用があったり、処理に時間かかる部分を実行させるのではなく、任意の値に変更する。 これにより、依存先の処理が成功・失敗したとき、どのように振る舞うかを簡単にテストすることができる。 そしてspyのように入出力情報に関心はない。


## 依存先がexport defaultを使っている場合

```typescript
export default function double(x: number): number {
  return 2 * x;
}
```


<a id="org8837c5e"></a>

### jest.mockをつかう

```typescript
import doubleSquare from '../src/doubleSquare';
import double from '../src/double';
jest.mock('../src/double');

describe('stub export default function with jest.mock', () => {
  beforeAll(() => {
    // doubleの返り値を、2に変更する
    (double as jest.Mock).mockReturnValue(2);
  })

  afterAll(() => {
    (double as jest.Mock).mockClear();
  })

  it('返り値は、4になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
})
```

spyのときとほとんど変わらないが、一点 `mockReturnValue()` が異なる。 このメソッドを使うことで、出力情報を変更している。

出力情報を変更するメソッドは他にも、 `mockResolveValue()`, `mockRejectValue()` などがある。 詳細なAPIについて、[公式サイト](https://jestjs.io/docs/ja/mock-function-api)参照。

### jest.spyOnをつかう

```typescript
import doubleSquare from '../src/doubleSquare';

describe('stub export default function with jest.spyOn', () => {
  let spy: jest.SpyInstance;
  beforeAll(() => {
    const org = require('../src/double');
    // spyOn()で返却されるインスタンスにmockReturnValueを使ってスタブにする
    spy = jest.spyOn(org, 'default').mockReturnValue(1);
  });

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

  it('戻り値は4になる。', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
});
```

spyOnでも同じである。jest.SpyInstanceは、mockと同じAPIをもつので、 `mockReturnValue()` を使って出力情報を変更する。


## 依存先がexportを使っている場合

```typescript
export function double(x: number): number {
  return 2 * x;
}
```

### jest.mockをつかう

```typescript
import doubleSquare from '../src/doubleSquare';
import { double } from '../src/double';
jest.mock('../src/double');

describe('stub export function with jest.mock', () => {
  beforeAll(() => {
    (double as jest.Mock).mockReturnValue(2);
  })

  afterAll(() => {
    (double as jest.Mock).mockClear();
  })

  it('返り値は、4になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
});
```

export defaultと全く同じでよい。

### jest.spyOnをつかう

```typescript
import doubleSquare from '../src/doubleSquare';

describe('stub export function with jest.spyOn', () => {
  let spy: jest.SpyInstance;
  beforeAll(() => {
    const org = require('../src/double');
    spy = jest.spyOn(org, 'double').mockReturnValue(1);
  });

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

  it('戻り値は4になる。', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
});
```

こちらもほとんどexport defaultのときと同じである。 spyOnの第二引数が `default` から `double` へと変わる。


# spyとstubの両方の機能がつかいたい

出力情報を変更して、入力情報もテストしたいというユースケースもあるだろう。 Jestはspy, stubを明確に区別しないので、今まで紹介してきたやり方で簡単に実現できる。

`spyOn`, `mock` どちらを使ってもよい。 というのも、どちらを使っても `.mock` で入力情報にアクセスでき、また `.mockReturnValue()` などの出力情報変更メソッドが使えるからである。 ここでは、依存先がexport defaultを使用しているケースでみていく。

```typescript
import doubleSquare from '../src/doubleSquare';
import double from '../src/double';
jest.mock('../src/double');

describe('stub export function with jest.mock', () => {
  beforeAll(() => {
    // 出力情報の変更
    (double as jest.Mock).mockReturnValue(1);
  })

  afterEach(() => {
    (double as jest.Mock).mockClear();
  });

  it('doubleが呼び出されるのは1回', () => {
    const actual = doubleSquare(3);
    assert.equal((double as jest.Mock).mock.calls.length, 1);
  });

  it('doubleの引数は3である', () => {
    const actual = doubleSquare(3);
    assert.equal((double as jest.Mock).mock.calls[0], 3);
  });

  it('double処理は正常終了し、例外をなげない', () => {
    const actual = doubleSquare(3);
    assert.equal((double as jest.Mock).mock.results[0].type, 'return');
  });

  it('doubleのreturn値は3である', () => {
    const actual = doubleSquare(3);
    assert.equal((double as jest.Mock).mock.results[0].value, 3);
  });

  it('返り値は、4になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
});
```

```typescript
import doubleSquare from '../src/doubleSquare';

describe('stub export function with jest.spyOn', () => {
  let spy: jest.SpyInstance;
  beforeAll(() => {
    const org = require('../src/double');
    // spyと同時に出力情報の変更
    spy = jest.spyOn(org, 'double').mockReturnValue(1);
  });

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

  // spyの確認
  it('呼び出されるのは1回', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.calls.length, 1);
  });

  it('引数は、3である', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.calls[0], 3);
  });

  it('処理は正常終了し、例外をなげない', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.results[0].type, 'return');
  });

  it('return値は3である', () => {
    const actual = doubleSquare(3);
    assert.equal(spy.mock.results[0].value, 3);
  });

  // stubの確認
  it('戻り値は4になる。', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
});
```

spyとかstubを意識しなくてよいので、非常に簡単である。 これがJestモックライブラリの良さである。

# mockした関数をもとにもどしたい

mockしたものをmockする前の状態に戻せる機能として、 `resetMock()` があるが、これは `spyOn` を適用した場合にのみ使用できる。 `jest.mock` を使った場合はもとに戻せない。ということで、実際にみていこう。

```typescript
import doubleSquare from '../src/doubleSquare';

describe('stub export default with jest.spyOn', () => {
  let spy: jest.SpyInstance;
  beforeEach(() => {
    const org = require('../src/double');
    spy = jest.spyOn(org, 'default').mockReturnValue(1);
  });

  afterEach(() => {
    //　テスト終了後にもとの関数にもどしている
    spy.mockRestore();
  });

  it('戻り値は、6になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 4);
  });
});

describe('mockした関数が前の状態に戻っているかどうか確認する', () => {
  let spy: jest.SpyInstance;
  beforeAll(() => {
    const org = require('../src/double');
    spy = jest.spyOn(org, 'default');
  });

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

  it('doubleの戻り値は、36になる', () => {
    const actual = doubleSquare(3);
    assert.equal(actual, 36);
  });
});
```

`jest.mock` を利用した場合、そのテストファイル内ではもとに戻すことはもうできない。
一方、`jest.spyOn` はそれが可能である。
各々テストを独立させ、影響及ぼさないようにしたいならば、 `spyOn` のほうが便利だろう。

# さいごに

以上、Jestのモックライブラリを使って、spy, stubを使用する方法を紹介しました。

依存先が `export` されているのか `export default` されているのかによって、微妙に書き方を変えないいけないので それぞれ紹介しました。

JestはxUnitのモック用語に準拠しないためspyだけしたいと考えると、少しばかり戸惑うことがあります。
しかし、特に区別せず大きくモックとして整理されていることを理解すると、非常に簡単に扱えるのが特徴です。
細かくモックを制御していきたいのであればsinonのほうがおすすめだが、そうでなければJestで十分と思える。

さいごにユースケースごとにどれを利用したほうがいいのか整理していきたいと思います。

## spy

| spy            | spyOn | mock |
|-------------- |----- |---- |
| export default | ○     | △    |
| export         | ○     | △    |

`spyOn` を利用したほうが個人的には間違いないと思う。 `mock` の場合、前準備のコード量が多くなること、 `jest.Mock` へのキャストを何度も要求されてしまうのに比べると、 `spyOn` のほうが楽に書くことができるからとなる。

## stub

| stub           | spyOn | mock |
|-------------- |----- |---- |
| export default | ○     | ○    |
| export         | ○     | ○    |

どちらも変わりないが、コード量を少なくしたいならば、 `mock` のほうが使いやすい。 ただ、 `jest.mock` 呼び出すだけでよく、入力情報を見る必要がないので、キャストする必要ないからだ。 入力情報を見る必要がないサードパーティライブラリはこちらを利用したほうがよいだろうと思う。

## spy & stub

| spy x stub     | spyOn | mock |
|-------------- |----- |---- |
| export default | ○     | △    |
| export         | ○     | △    |

こちらもほとんど差がないが、spyのときの評価に加えて `spyOn` を使ったほうがリストアできるという点は非常に魅力的である。
前述したが、あるテストが他のテストに影響与えないようにするのが理想的なので、リストアできるというのはこの点で素晴らしい。

サードパーティのライブラリをモックする場合、 `mock` が便利となり、それ以外は、 `spyOn` 使いこなせれば大抵困らないと思う。


# 参考書籍・サイト

-   [テスト駆動開発](https://www.amazon.co.jp/dp/B077D2L69C/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)
-   [Jest](https://jestjs.io/ja/)