テスト初心者が初めに覚えた依存モジュールのモック化5パターン（Jest + TypeScript)

# 付録Cでテストに対する考え方が変わりました！

社会人になって会社で学んだ単体テストは、Excelで全条件分岐の組み合わせ表(マトリクス)を作成し、デバッガ(gcc)のステップ実行で目視確認、マトリクスのセルに○印をつけていくというスタイルです。コード中のあり得ない分岐もデバッガで値を変えて無理やりカバレッジ100%を達成する必要があり、嫌でもデバッガと仲良くなります。もちろんコードに修正がはいれば全てやり直しという過酷なものでした。
その苦い思い出以来、なにかと言い訳して極力テストを書かずに逃げてきました。。しかし、最近いろいろなところから「[テスト駆動開発](http://amzn.asia/9IIgL5X)の付録Cが素晴らしい！」という評判を聞いて、遅ればせながら読んでみたところ、テストに対する考え方が180度変わりました。
色々な方が[書評](https://qiita.com/ex_SOUL/items/68dee0254732b7c623f1)を書いておりますので詳細は省略しますが、私は付録Cを読んだ翌日からおもわず開発にTDDを取り入れ、すっかりテストが好きになってしまいました。

# テスト対象が依存するモジュールのモック化
テストを書くモチベーションは十分に高まったのですが、いままでテストケースを書いたことがないし、先輩方も似たような状況なので、いろいろとつまづきました。
特にテスト対象が依存するモジュールのモック化で手間取ることが多かったので、モック化のパターンをまとめてみました。私のようなテスト初心者の助けになれば幸いです。
誤りやもっとよい方法があれば**編集リク**を頂けると嬉しいです :bow: 
サンプルはNode.jsのアプリで、TypeScript (v2.6), Jest (v21.2.1)で書きました（Jestの概要は「[ついに jest の軍門に降った](https://qiita.com/karak/items/9d0ebf7bc50085624913)」や[公式サイト(日本語）](http://facebook.github.io/jest/ja/)でどうぞ）。
### もっともシンプルなパターン
サンプルのテスト対象は、引数で指定されたファイルを読み込んで行数を返す関数`LineCount()`です。`LineCount()`はモジュール`fs`に依存しており、`fs.readFileSync()`をモック化したいとします。

```typescript:app.ts(テスト対象)
import * as fs from 'fs'; // <-- テスト対象が依存するモジュール

export function LineCount (path: string): number {
    const data = fs.readFileSync(path, 'utf8'); // <-- readFileSync()をモック化したい
    return data.split('\n').length;
}

export default LineCount;  
```

この場合、テストコードは下記のようになります。

```typescript:app.spec.ts
// (1): テスト対象が依存するモジュールのモックを定義
jest.mock('fs', () => ({
        readFileSync: jest.fn(() => `first¥n second¥n third`),
}));
// (2): 検証用((4),(5))にモック読み込む
import * as fs from 'fs';          

// (3): (1)を使うテスト対象を読み込む
import {LineCount} from '../app';

describe('サンプル', () => {
    const path = 'dummy';
    it('LineCount()', () => {
        const result = LineCount(path);

        expect(result).toBe(3);
        expect(fs.readFileSync.mock.calls.length).toBe(1); // (4): モックが呼ばれたか検証
        expect(fs.readFileSync.mock.calls[0][0]).toBe(path); // (5): モックが呼ばれた際の引数を検証
    });
});
```

ポイントは以下のとおり。

 * テスト対象を読み込む前に`jest.mock()`でテスト対象が依存するモジュールをモック化し、振る舞いを定義します（①は③より前)。
 * モック化したモジュールを検証用に読み込む場合はモック化の後に読み込みます（①は②より前）。

### デフォルトエクスポートのパターン
モジュールによっては、`export default XXX`という形でモジュールが公開されている場合があります。その場合はモックする際に`default`プロパティをつくって、その中にモックを作成する必要があります。
デフォルトエクスポートかどうかわからない場合は、下記のように両方に対応させておくと良いかと思います。

```typescript:app.spec.ts
// (1): テスト対象が依存するモジュールのモックを定義
jest.mock('fs', () => {
    const mockFn = jest.fn(() => `first¥n second¥n third`);
    return {
        default: { // デフォルトエクスポート
            readFileSync: mockFn,
        },
        readFileSync: mockFn, // デフォルトエクスポートじゃない
    };
});
// (2)-(5)は変更なし
```

### 非同期関数(callback系)のパターン
次のテスト対象のサンプル`LineCountAsync()`は、先の`LineCount()`とは異なり、内部で非同期型の`fs.readFile()`を呼び出しています。この`fs.readFile()`をモック化したいとします。


```typescript:app.ts
import {promisify} from 'util';
import * as fs from 'fs';
const readFilePromise = promisify(fs.readFile); // 非同期関数をプロミス化

export async function LineCountAsync (path: string): Promise<number> {
    const data = await readFilePromise(path, 'utf8'); // <-- モック化したい
    return data.split('\n').length;
}
```

うえの例では今時っぽく`promisify()`でプロミス化してawaitやasyncを利用していますが、プロミス化しない場合でもモックの書き方は同じです。`LineCountAsync()`の呼び出しを`return`で返すようにしないと、非同期処理が走るまえにテストが終了してしまうので要注意です。


```typescript:app.spec.ts
// (1): テスト対象が依存するモジュールのモックを定義
jest.mock('fs', () => ({
        readFile: jest.fn((path, opt, cb) => { // 非同期関数のモック
            cb(null, `first¥n second¥n third`);  // コールバック関数の引数にの戻り値を渡す
        }),
}));

// 省略

describe('test', () => {
    it('LineCountAsync', () => {
        return LineCountAsync(path).then(result => {
            expect(result).toBe(3);
        });
    });
});
```

### 各テストケースでモックの振る舞いを変えるパターン
いままでの例だと、一番初めにモックの振る舞いを定義するので、すべてのテストケースでモックの動作が同じになります。モックの振る舞いを変えるには、テストケース内で`jest.fn().mockImpelementation()`で定義するのが良いと思います。また、`beforeEach`でテストケース毎にモックを初期化(`mockClear()`)しましょう。

```typescript:app.spec.test
// (1),(2),(3)はそのまま
describe('test', () => {           
    beforeEach(() => {             
        fs.readFileSync.mockClear();                                   
    });    
    it('ファイルが空', () => {
        fs.readFileSync.mockImplementation(() => ''); // 空文字を返す振る舞いを定義
        const result = LineCount(path);
        expect(result).toBe(1); 
    });
    it('ファイルが空でない', () => {
        fs.readFileSync.mockImplementation(() => 'hello¥n world'); // 文字列を返す振る舞いを定義
        const result = LineCount(path);
        expect(result).toBe(1);
    });
});
```


### １つのテストケース内でモックの振る舞いを変えるパターン

次のテスト対象のサンプル`LineCountDiff()`は、２つのファイルの行数の差を返す関数で、内部では`readFileSync`を２回読んでいます。`readFileSync()`をモック化して１回目と２回目で異なる値を返したいとします。

```typescript:app.ts
export function LineCountDiff (path1: string, path2: string): number {
    const data1 = fs.readFileSync(path1, 'utf8'); // １回目
    const data2 = fs.readFileSync(path2, 'utf8'); // ２回目
    return data1.split('\n').length - data2.split('\n').length;
}
```

モックが呼ばれるたびに異なる値を返すようにするには、ジェネレータが便利です。

```typescript:app.spec.ts
    it.only('LineCountDiff', () => {
        const path2 = 'dummy2';
        const generator = function* (e) { yield *e; };
        const mockFn = generator(['1¥n2¥n3', '1']); //１回目は'1¥n2¥n3¥n'、２回目は'1'を返す

        fs.readFileSync.mockImplementation(() =>  mockFn.next().value); // ジェネレータでモック化

        const result = LineCountDiff(path, path2);
        expect(result).toBe(2);
    });
```

--

以上のソースコードは[Gist](https://gist.github.com/vimyum/84493cdcd762f5cc909f2b38003e8190)にものせております。