仕事で全くテストコードを書いていない&書き方を知らないので、一から書き方を勉強しようのコーナー。
tl;td
- FizzBuzzを書く
- テストしやすいように書き直す
- テストする
下準備
今回使うテストフレームワークはJestになります。
サクッと準備してしまいましょう。
なお、npmの環境は準備できているものとします。
$ npm init
$ npm install --save-dev jest
{
"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
を出力する(このとき、Fizz
、Buzz
は出力しない)
コードを書く
それではコードを書いていきましょう。
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()
です。これを分離してみましょう。
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メソッドの詳細に踏み込んだテストが必要になった時は、設計に何か問題がある可能性が高いそうです。つまり、テストが難しいときは設計を見直してみることも一つの手です。
では、上記に従ってファイルを分割してみましょう。
const toFizzBuzz = require('./modules/fizzbuzz');
const app = () => {
for (let i = 1; i <= 100; i++) {
console.log(toFizzBuzz(i));
}
};
module.exports = app;
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の倍数でもないとき、整数をそのまま返却する
仕様が書けたなら、フローチャートに書き起こしてみましょう。
フローチャートはPlantUMLを使って書いてみました。サクッとかけるので便利ですね。
では、このフローチャートを満たすようにテストコードを書いていきましょう。
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);
| ^
はい引っかかりました。当然ですね。引数の型制限処理なんて書いてませんから。
このように、仕様の設定とテストコードをしっかり押さえておけばコードの動作を保証してくれるので、うっかりミスが激減します。テストを書く大きなメリットの一つでしょう。
テストの効果を確認できたところで、コードをテストに通るように書き直していきましょう。
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%は無理に目指すものではないそうですが、参考にしていきたいですね。