Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
126
Help us understand the problem. What is going on with this article?
@jintz

jestでユニットテスト基礎

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

tl;td

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

下準備

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

$ npm init
$ npm install --save-dev jest
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は入れておきましょう。速度重視で雑に書いても整形してくれるのが便利すぎてもう戻れない。

仕様を決める

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

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

コードを書く

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

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とロジックを分離することらしいので、その通りに書き直してみましょう。

コードを書き直す

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

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メソッドの詳細に踏み込んだテストが必要になった時は、設計に何か問題がある可能性が高いそうです。つまり、テストが難しいときは設計を見直してみることも一つの手です。

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

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

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

module.exports = app;
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に焦点を絞りましょう。

テストする

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

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

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

fizzbuzz.png

フローチャートはPlantUMLを使って書いてみました。サクッとかけるので便利ですね。
では、このフローチャートを満たすようにテストコードを書いていきましょう。

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コマンドを登録しておいたのでこれを使います。

$ 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);
        |                                    ^

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

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

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

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;

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

 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.

通りました。やったね!

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

おまけ

今回はテストフレームワークとしてJestを利用しました。Jestの特徴はいくつかありますが、ここではカバレッジについて紹介したいと思います。

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

$ npm run test -- --coverage

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

126
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
jintz
こいついつも何か始めてんな。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
126
Help us understand the problem. What is going on with this article?