Node.js + Jest でモジュールの関数とクラスをモック化してユニットテストするサンプルコード

## 概要

- JavaScript テストフレームワークの Jest を使う
- jest.mock を使用してモックを作成し、ユニットテストを実行する

## npm init で開発用ディレクトリを用意

```console
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (hello) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/me/hello/package.json:

{
  "name": "hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes) 
```

## Jest をインストール

```console
$ npm install --save-dev jest

> fsevents@1.2.8 install /Users/me/hello/node_modules/fsevents
> node install

node-pre-gyp WARN Using request for node-pre-gyp https download 
[fsevents] Success: "/Users/me/hello/node_modules/fsevents/lib/binding/Release/node-v64-darwin-x64/fse.node" is installed via remote
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN hello@1.0.0 No description
npm WARN hello@1.0.0 No repository field.

+ jest@24.7.1
added 558 packages from 373 contributors and audited 850808 packages in 12.066s
found 0 vulnerabilities

$ cat package.json 
{
  "name": "hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^24.7.1"
  }
}
```

package.json の scipts.test の箇所を Jest を実行するように書き換える。

```console
$ cat package.json 
{
  "name": "hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^24.7.1"
  }
}
```

## サンプルコードを用意

### getrandomint.js

```getrandomint.js
// ランダムな値を返す関数
function getRandomInt(max) {
  return Math.floor(Math.random() * Math.floor(max));
}

module.exports = getRandomInt;
```

### getrandomint.test.js

```getrandomint.test.js
const getRandomInt = require('./getrandomint');

// ランダムな値ではテストできないので
// 指定した値を返すモック関数を作成
jest.mock('./getrandomint');
const getRandomIntMock = jest.fn((max) => {return max;});
getRandomInt.mockImplementation(getRandomIntMock);

// 指定した値が返ってくるかテスト
test('getRandomInt', () => {
  expect(getRandomInt(100)).toBe(100);
});
```

### random.js

```random.js
// ランダムな値を返すクラス
class Random {
  constructor() {
  }
  getInt(max) {
    return Math.floor(Math.random() * Math.floor(max));
  }
}

module.exports = Random;
```

### random.test.js

```random.test.js
const Random = require('./random');

// ランダムな値ではテストできないので
// 指定した値を返すモッククラスを作成
jest.mock('./random');
const getIntMock = jest.fn((max) => {return max});
Random.mockImplementation(() => {
  return {
    getInt: getIntMock,
  };
});

// 指定した値が返ってくるかテスト
test('Random.getInt', () => {
  expect(new Random().getInt(100)).toBe(100);
});
```

### foobar.js

``` foobar.js
// getRandomInt 関数と Random クラスを使用する
const getRandomInt = require('./getrandomint');
const Random = require('./random');

// ランダムな数を2つ足して返す関数
function foobar(num) {
  const a = getRandomInt(num);
  const b = new Random().getInt(num);
  return a + b;
}

module.exports = foobar;
```

### foobar.test.js

```foobar.test.js
// getRandomInt 関数を読み込む
const getRandomInt = require('./getrandomint');

// ランダムな値ではテストできないので
// 指定した値を返すモック関数を作成
jest.mock('./getrandomint');
const getRandomIntMock = jest.fn((max) => {return max;});
getRandomInt.mockImplementation(getRandomIntMock);

// Random クラスを読み込む
const Random = require('./random');

// ランダムな値ではテストできないので
// 指定した値を返すモッククラスを作成
jest.mock('./random');
const getIntMock = jest.fn((max) => {return max});
Random.mockImplementation(() => {
  return {
    getInt: getIntMock,
  };
});

// getRandomInt 関数とRandom クラスを使用している foobar 関数を読み込む
const foobar = require('./foobar');

// 100を指定したら200が返ってくるかテスト
// foobar 関数の内部では getRandomInt 関数と Random クラスのモックが呼び出されている
test('foobar(100)', () => {
  expect(foobar(100)).toBe(200);
});
```

## テストを実行する

```console
$ npm test

> hello@1.0.0 test /Users/me/hello
> jest

 PASS  ./random.test.js
 PASS  ./getrandomint.test.js
 PASS  ./foobar.test.js

Test Suites: 3 passed, 3 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.64s
Ran all test suites.
```

## 今回の環境

macOS Mojave + Node.js v10.15.3 + Jest 24.7.1

```console
$ node --version
v10.15.3

$ ./node_modules/jest/bin/jest.js --version
24.7.1

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.4
BuildVersion:	18E226
```

## 参考資料

- [Jest · 🃏 Delightful JavaScript Testing](https://jestjs.io/)
- [Getting Started · Jest](https://jestjs.io/docs/ja/getting-started)
- [Mock Functions · Jest](https://jestjs.io/docs/ja/mock-functions)
- [ES6 Class Mocks · Jest](https://jestjs.io/docs/ja/es6-class-mocks)
- [Mock Functions · Jest](https://jestjs.io/docs/ja/mock-function-api)