はじめに
テスト駆動開発(Test-Driven Development: TDD)という開発手法があります。
この手法は、プログラムの実装前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく方法になります。
テスト駆動開発は、「レッド」「グリーン」「リファクタリング」という順序で進めることができますが、
- 【レッド】実装した機能の要件を元に失敗するテストコードを書く
- 【グリーン】どんな方法でも良いのでテストが成功するコードを書く
- 【リファクタリング】テストが成功する状態を維持しつつ簡潔・明快なコードにする
これにより、「後工程へバグを持ち越しにくい」、「システムの要件をより深く理解できる」、「開発者が安心してコーディングでき、心理的負担が減る」というメリットを享受することができます。
今回はこのTDDの手法を基礎から学んでみようと、「FizzBuzz問題」を題材にして実装を進めてみたいと思います。
今回の記事を書くにあたり、下記の書籍や記事を参考にしました。
FizzBuzz問題とは
"FizzBuzz"とは英語圏の有名な言葉遊びです。ルールは1から順に数を数え上げていき、3の倍数なら「Fizz」、5の倍数なら「Buzz」、両方の倍数(15の倍数)なら「Fizz Buzz」、いずれでもなければその数を言うというものです。
簡単なプログラミングの練習として、3の倍数のときは「"fizz"」、5の倍数のときは「"buzz"」、共通の倍数のときは「"fizzbuzz"」、その他は「数値」を戻すという単純な処理を実装します。
TDDの基本手順
FizzBuzz問題をTDDで実装するにあたり、テスト駆動開発の本に記載の内容に沿って下記の手順で進めます。
1. TODOリストを書く
2. テストを一つ書く
3. すべてのテストを走らせ、新しいテストの失敗を確認する
4. 小さな変更を行う
5. すべてのテストを走らせ、すべて成功することを確認する
6. リファクタリングを行って重複を除去する
要件
FizzBuzz問題の要件として、下記を定義します。
1. 数が3で割り切れる時、「Fizz」を出力する。
2. 数が5で割り切れる時、「Buzz」を出力する。
3. 数が3でも5でも割り切れる時、「FizzBuzz」を出力する。
4. 上記以外の数値の場合、渡された数値をそのまま出力する。
実装
手順に沿って実装をしていきます。
TODOリストを書く
事前準備と要件に沿ったTODOを残します。
- jestのインストール
- FizzBuzzを実装するファイルとテストを書くファイルをそれぞれ準備
- 「渡された数が3で割り切れる時、Fizzを返す」のテストを記述
- Counterクラスを実装
- 「渡された数が3で割り切れる時、Fizzを返す」の実装
- 「渡された数が5で割り切れる時、Buzzを返す」のテストを記述
- 「渡された数が5で割り切れる時、Buzzを返す」を実装
- 「渡された数が3でも5でも割り切れる時、FizzBuzzを返す」のテストを記述
- 「渡された数が3でも5でも割り切れる時、FizzBuzzを返す」を実装
TODOが完了したら、打ち消し線などを引いてDONEにしていきます。
途中でTODOが増えたら都度追加します。
事前準備
まずは事前準備です。
- 空のプロジェクトを作成し、jestおよび必要な設定をインストールします。
$ mkdir training
$ cd training
$ yarn init
$ yarn add --dev typescript jest @types/jest ts-jest
$ yarn ts-jest config:init
最後にpackage.jsonに以下のようにjestの実行スクリプトを追加して、 yarn run test
を実行すると単体テストが全て実行されるようになります。
{
(中略)
"scripts": {
"test": "jest"
},
(中略)
}
- FizzBuzzを実装するファイル「fizzbuzz.ts」とテストを書くファイル「fizzbuzz.test.ts」を準備します。
jestのインストール等の手順については下記記事を参考にしました。
テストを一つ書く
まずはじめに「渡された数が3で割り切れる時、Fizzを返す」のテストを記述し、yarn run test
で実行します。
describe("FizzBuzz", () => {
it("渡された数が3で割り切れる時、Fizzを返す", () => {
expect(new Counter().print(3)).toEqual("Fizz")
})
});
まだCounterクラスが実装されていないので、下記のエラーが出力されます。
Counterクラスを実装
1つ目のテストが書けたので、Counterクラスを実装していきます。
export class Counter {
print(count: number): string {
if (count % 3 === 0) {
return "Fizz"
}
return count.toString()
}
}
Counterクラスをテストにインポートします。
import { Counter } from '../src/fizzbuzz'
describe("FizzBuzz", () => {
it("渡された数が3で割り切れる時、Fizzを返す", () => {
expect(new Counter().print(3)).toEqual("Fizz")
})
});
yarn run test
を実行すると下記の様な結果が出力され、テストが通っていることがわかりました。
小さなリファクタリングを行っていく
簡単なコードの為、ここでは省略します。
サイクルを繰り返す
上記までのサイクルを要件が全て満たされるまで繰り返します。
テストが全て通るコードが実装できました。
条件分岐が増える事を見越したリファクタリングまでできると良さそうでしたが、まずは入門という事でここまでの実装とします。
export class Counter {
print(count: number): string {
if (count % 3 === 0 && count % 5 === 0) {
return "FizzBuzz"
}
else if (count % 3 === 0) {
return "Fizz"
}
else if (count % 5 === 0) {
return "Buzz"
}
return count.toString()
}
}
import { Counter } from '../src/fizzbuzz'
describe("FizzBuzz", () => {
it("渡された数が3で割り切れる時、Fizzを返す", () => {
expect(new Counter().print(3)).toEqual("Fizz")
})
it("渡された数が5で割り切れる時、Buzzを返す", () => {
expect(new Counter().print(5)).toEqual("Buzz")
})
it("渡された数が3または5で割り切れる時、FizzBuzzを返す", () => {
expect(new Counter().print(15)).toEqual("FizzBuzz")
})
it("渡された数が3、5、15で割り切れる数値である場合、FizzBuzzを返す", () => {
expect(new Counter().print(45)).toEqual("FizzBuzz")
})
it("渡された数が3、5、15のどれでも割り切れないとき、数字をそのまま返す", () => {
expect(new Counter().print(1)).toEqual("1")
})
});
テストケースのバリエーションを増やす
必要に応じて、テストケースのバリエーションを増やしていきます。
今回は入力値の上限、下限は考えなくても良かったのですが、もし「1〜100までの数値」というような入力制限があった場合、同値クラスや境界値などのテストも検討できると良さそうです。
- Fizzを返す場合: 3, 39 など
- Buzzを返す場合: 5, 20 など
- FizzBuzzを返す場合: 15, 45 など
- 数値をそのまま返す場合: 1, 2, 99, 100 など
- エラーを返す場合:1未満の数値、 100より大きい数値、数値以外の値を渡した場合 など
FizzBuzz問題におけるテストケースの検討についてはこちらの記事も参考にしています。
おわりに
FizzBuzz問題を題材として、TDDの基礎を学びました。
1つ1つテストコードを書いていくことで、要件の把握が容易になる、実装時のテスト不足による致命的な不具合を予防できるなどの良い点は多くありそうに思いました。
シンプルなプログラムでしたが、対象が複雑になればなるほどそれだけテストのスキルや観点も多く必要になってくると思います。また良い題材があればTDDの練習としてチャレンジしてみたいです。