jestでユニットテスト基礎

仕事で全くテストコードを書いていない＆書き方を知らないので、一から書き方を勉強しようのコーナー。

# tl;td

- FizzBuzzを書く
- テストしやすいように書き直す
- テストする

# 下準備

今回使うテストフレームワークはJestになります。
サクッと準備してしまいましょう。
なお、npmの環境は準備できているものとします。

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

```json:package.json
{
  "name": "fizzbuzz_tester",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "jest"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^24.1.0",
  }
}

```

これだけで十分です。でも[ESLintとPrettierは入れておきましょう](https://qiita.com/khsk/items/0f200fc3a4a3542efa90)。速度重視で雑に書いても整形してくれるのが便利すぎてもう戻れない。

# 仕様を決める

コードを書く前に、まずは仕様を決定しましょう。今回はみんな大好きFizzBuzzを作るとします。

- 1～100までの整数を1から順番に出力する
- 整数が3の倍数のとき、整数の代わりに`Fizz`を出力する
- 整数が5の倍数のとき、整数の代わりに`Buzz`を出力する
- 整数が15の倍数のとき、整数の代わりに`FizzBuzz`を出力する（このとき、`Fizz`、`Buzz`は出力しない）

# コードを書く

それではコードを書いていきましょう。

```javascript:app.js
const app = () => {
  for (let i = 1; i <= 100; i++) {
    if (i % 3 == 0 && i % 5 == 0) {
      console.log('FizzBuzz');
      continue;
    }

    if (i % 3 == 0) {
      console.log('Fizz');
      continue;
    }

    if (i % 5 == 0) {
      console.log('Buzz');
      continue;
    }

    console.log(i);
  }
};

module.exports = app;
```

実行すればちゃんと動きますね。要求されたものとしては問題ありません。

さて、テストをするにはどうすればいいでしょうか？
答えは「まずコードを書き直せ」です。このままではテストが難しい構造となっているからです。
[ユニットテストの基本はI/Oとロジックを分離すること](https://qiita.com/koduki/items/4fde43b68fe450c6a5d8)らしいので、その通りに書き直してみましょう。

# コードを書き直す

上記のコードに含まれるI/Oといえば、もちろん`console.log()`です。これを分離してみましょう。

```javascript:app.js
const app = () => {
  for (let i = 1; i <= 100; i++) {
    console.log(toFizzBuzz(i));
  }
};

const toFizzBuzz = num => {
  if (num % 3 == 0 && num % 5 == 0) return 'FizzBuzz';
  if (num % 3 == 0) return 'Fizz';
  if (num % 5 == 0) return 'Buzz';
  return num;
};

module.exports = app;
```

`toFizzBuzz`にFizzBuzzのルールを格納し、処理の流れと結果の出力は`app`に任せることにしました。

これでI/Oが分離できましたが、実はまだテストできません。
上記のコードでは、モジュールとして開放しているのは`app`のみです。ロジックの主体となる`toFizzBuzz`をテストするにはこちらもモジュールとして開放する必要がありますが、仕様に含まれていないものを不用意に露出させるべきではないでしょう（このままでもテストできるようなライブラリもあるそうですが……）。

ではどうすればいいのか？ 方法はいくつかありますが、今回は`toFizzBuzz`を別ファイルに切り出します。
[privateメソッドの詳細に踏み込んだテストが必要になった時は、設計に何か問題がある可能性が高い](http://su-kun1899.hatenablog.com/entry/2016/09/05/010104)そうです。つまり、テストが難しいときは設計を見直してみることも一つの手です。

では、上記に従ってファイルを分割してみましょう。

```javascript:app.js
const toFizzBuzz = require('./modules/fizzbuzz');

const app = () => {
  for (let i = 1; i <= 100; i++) {
    console.log(toFizzBuzz(i));
  }
};

module.exports = app;
```

```javascript:modules/fizzbuzz.js
const toFizzBuzz = num => {
  if (isFizz(num) && isBuzz(num)) return 'FizzBuzz';
  if (isFizz(num)) return 'Fizz';
  if (isBuzz(num)) return 'Buzz';
  return num;
};

const isFizz = num => {
  return num % 3 == 0;
};

const isBuzz = num => {
  return num % 5 == 0;
};

module.exports = toFizzBuzz;

```

こうなるとだいぶすっきりしましたね。`fizzbuzz.js`のテストについて考えるのは難しくなさそうです。
`app.js`についてですが、`console.log`など既に品質が保障されているものはテスト不要ですので、テストするべき箇所は正しくfor文を実行できているかについてのみです。こちらはモックを利用してテストするべきですが、長くなってしまうので今回は省いて`fizzbuzz.js`に焦点を絞りましょう。

# テストする

ではテストコードを書いていきます。ブラックボックステストでは[同地分割や境界値分析](https://qiita.com/softest/items/648d8bb4021cd1256b02)について考える必要があるそうですが、今回はホワイトボックステストを行います。というのも、[ホワイトボックステストはコード作成者が実装するもので、そのコードがどういう動作をするべきかを保証するもの](https://qiita.com/seki_uk/items/beb5f9276faa2163db4e)だからです。
ホワイトボックステストにおけるコードの動作についてはフローチャートを作成するとわかりやすいようです。フローチャートを作るために、まずはこの`fizzbuzz.js`に必要な仕様を考えてみましょう。

- 1つの引数`num`を受け取る
- 引数が整数でないとき、エラーを出力する
- 整数が1未満のとき、エラーを出力する
- 整数が15の倍数のとき、`FizzBuzz`を返却する
- 整数が3の倍数のとき、`Fizz`を返却する
- 整数が5の倍数のとき、`Buzz`を返却する
- 整数が3の倍数でも5の倍数でもないとき、整数をそのまま返却する

仕様が書けたなら、フローチャートに書き起こしてみましょう。

![fizzbuzz.png](https://qiita-image-store.s3.amazonaws.com/0/213374/9f1c3dc2-48ba-89c0-554a-f1909d6ffd5f.png)

フローチャートは[PlantUML](https://qiita.com/couzie/items/9dedb834c5aff09ea7b2)を使って書いてみました。サクッとかけるので便利ですね。
では、このフローチャートを満たすようにテストコードを書いていきましょう。

```javascript:modules/fizzbuzz.test.js
const toFizzBuzz = require('./fizzbuzz');

test('fizzbuzzのユニットテスト', () => {
  expect(() => toFizzBuzz('test')).toThrow(RangeError);

  expect(() => toFizzBuzz(-1)).toThrow(RangeError);
  expect(() => toFizzBuzz(0)).toThrow(RangeError);

  expect(toFizzBuzz(1)).toBe(1);
  expect(toFizzBuzz(3)).toBe('Fizz');
  expect(toFizzBuzz(5)).toBe('Buzz');
  expect(toFizzBuzz(15)).toBe('FizzBuzz');
});
```

何をテストしているかは感覚的にわかると思います。
では実行してみましょう。package.jsonに`test`コマンドを登録しておいたのでこれを使います。

```console:
$ npm run test

 FAIL  middleware/fizzbuzz.test.js
  ✕ fizzbuzzのユニットテスト (4ms)

  ● fizzbuzzのユニットテスト

    expect(received).toThrow(expected)

    Expected name: "RangeError"

    Received function did not throw

      2 |
      3 | test('fizzbuzzのユニットテスト', () => {
    > 4 |   expect(() => toFizzBuzz('test')).toThrow(RangeError);
        |                                    ^
```

はい引っかかりました。当然ですね。引数の型制限処理なんて書いてませんから。

このように、仕様の設定とテストコードをしっかり押さえておけばコードの動作を保証してくれるので、うっかりミスが激減します。テストを書く大きなメリットの一つでしょう。

テストの効果を確認できたところで、コードをテストに通るように書き直していきましょう。

```javascript:middleware/fizzbuzz.js
const toFizzBuzz = num => {
  if (outOfRange(num))
    throw new RangeError('引数が不正です。1以上の整数のみ引数に指定可能です。');

  if (isFizz(num) && isBuzz(num)) return 'FizzBuzz';
  if (isFizz(num)) return 'Fizz';
  if (isBuzz(num)) return 'Buzz';
  return num;
};

const outOfRange = num => {
  if (!(typeof num === 'number')) return true;
  if (Math.round(num) != num) return true;
  if (num < 1) return true;

  return false;
};

const isFizz = num => {
  return num % 3 == 0;
};

const isBuzz = num => {
  return num % 5 == 0;
};

module.exports = toFizzBuzz;
```

書き直せたら再びテストを実行します。

```console
 PASS  middleware/fizzbuzz.test.js
  √ fizzbuzzのユニットテスト (15ms)

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

通りました。やったね！

当然ですが、テストに通れば絶対にバグが起きない、なんてことはありません。[テストはあくまでテストした範囲で問題が無いことしか示せませんから](https://webrage.jp/techblog/7_principles_of_software_testing/)。
とはいえ、自動でテストを行えるようになれば様々な面で恩恵を受けられるので、是非ともテストを書く練習を続けていきたいところですね。

# おまけ

今回はテストフレームワークとしてJestを利用しました。[Jestの特徴はいくつかあります](https://qiita.com/karak/items/9d0ebf7bc50085624913)が、ここではカバレッジについて紹介したいと思います。

テストを走らせる際にオプションを一つ追加してみましょう。

```console
$ npm run test -- --coverage
```

`--coverage`オプションはテストのカバレッジについて自動で調査し、結果を出力してくれます。ワーキングディレクトリに`coverage`というディレクトリが生成され、その中の`Iconv-report/index.html`を開くと詳細がわかります。
カバレッジの詳細やテストが足りていない部分が一目でわかります。ちなみに今回私が書いたテストコードも不十分で、しっかり指摘されてしまいました。
カバレッジ100%は無理に目指すものではないそうですが、参考にしていきたいですね。