テスト駆動開発
本書は、テスト駆動開発(Test-Driven Development)について書かれている。
第一部では、TDDで書かれた典型的なモデルのコードを例示している。実際のコードを見ることで、TDDのイメージが付く。それにより、テストコードを先に書き、設計を有機的に成長させる方法を学ぶ。
第二部では、より複雑なロジック(リフレクションや例外のテスト等)のテスト方法を、自動テストのフレームワークを実際に開発しながら学ぶ。
第三部では、どのようなテストを書くのかの判断に関するパターン、xUnitを使用したテストの書き方のパターン、リファクタリング方法などを学ぶ。
テストとは
動詞では「評価する」、名詞では「合格か不合格かを判定する手順」という意味合いである。
テストを頻繁に走らせることで、ミスをしでかす確率を減らし結果的にストレスが減っていく。
TDDとは
テストファーストなプログラムの開発手法。プログラムを実装する前にテストコードを書き、そのテストコードに適合するように実装とリファクタリングを進めていく。
TDDの目的
動作するきれいなコードを書くことがTDDの目的である。きれいなコードを書くことで以下のような価値が生まれる。
- 完成したかどうかが分かるため、バグが残っているか心配する必要がなくなる
- コードを余すことなく理解することができる
- チームメイトとの信頼が深まる
- 書いていて気持ちが良い
TDDはプログラミング中の不安をコントロールする手法である。
不安は「ためらいを生む」「コミュニケーションを減らす」「フィードバックから逃げ腰にさせる」等悪い効果がある。
テストが動くことを確認しながら実装することで不安を取り除くことができる。
TDDのルール
TDDのルールは2つだけである。
・ 自動化されたテストが失敗したときのみ、新しいコードを書く
・ 重複を削除する
この2つのルールはプログラミングにおける作業の順序でもある。
また、これにより以下のような価値が生まれる。
- 有機的に設計を進めることができる
- 動作するコードが設計判断にフィードバッグをもたらすからである。
- 自分たちでテストを書くようになる
- 誰かがテストを書いてくれるのを待つことができないからである。
- 小さな変更に迅速に応答する開発環境を備える必要が出る
流れ
まずはTODOリストを作成する
テストを作成する前に、必要になりそうなテストをリストに書き出しておく必要がある。
どこに行くべきかがわかるまでは、一歩も踏み出さないことが重要である。
TDDを行う上でのTODOリストの作成方法は以下の通りである。
- 実装しなければならない振る舞いを考えられるだけ書き出してみる。
- まだ実装がない操作に関して、空実装をリストに加える。
- いま書いたばかりのコードをきれいにするためのリファクタリングをすべて書き出しておく。
TDDの順序
TDDの進め方は以下の通りである
- テストを一つ書く
- すべてのテストを走らせ、新しいテストの失敗を確認する
- 小さな変更を行う
- すべてのテストを走らせ、すべて成功することを確認する
- リファクタリングを行って重複を削除する
例
掛け算のテストを作成する。
まず **「1. テストを一つ書く」**から始める
public class MoneyTest {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}
上記のコードはコンパイルすらできない。
a. Dollarクラスが無い
b. コンストラクタが無い
c. timesメソッドが無い
d. amountフィールドが無い
からである。
これを一つずつ解決していく。
// a.を解決
class Dollar {
// d.を解決
int amount;
// b.を解決
Dollar (int amount) {
}
// v.を解決
void times(int multiplier) {
}
}
コンパイルが通るようになれば、テストを動かす。
five.amountは0なので当然テストは失敗する。
「2. すべてのテストを走らせ、新しいテストの失敗を確認する」である。
そのため、「3. 小さな変更を行う」!!
とりあえずは、テストを通すことを優先させる。
テストを通すための最小限の変更は以下の通りである
class Dollar {
int amount = 10;// 値をベタ書きする
Dollar (int amount) {
}
void times(int multiplier) {
}
}
当然テストは動く。
**「4. すべてのテストを走らせ、すべて成功することを確認する」**である。
- から4. まで終わったので次は**「5. リファクタリングを行って重複を削除する」**
テストコードとプロダクトコードで「5」と「2」が重複している。
// MoneyTest.java
public class MoneyTest {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}
// Dollar.java
class Dollar {
int amount = 10; // 5 * 2
そのため、Dollar.javaをリファクタリングする
class Dollar {
int amount;
Dollar (int amount) {
this.amount = amount;
}
void times(int multiplier) {
amount += multiplier;
}
}
このように細かいステップを踏むことでプロダクトコードが完成していく。
TDDは細かいステップを踏み続けることが重要である。
「1. テストを一つ書く」~「5. リファクタリングを行って重複を削除する」のステップを踏み続ける。
まとめ
- テストを書く
- 頭の中で想像した操作がどんなコードとして現れるかを考える。
- テストを動かす
- テストがすべて通る状態へと素早く到達する。もし実現に時間が掛かりそうな場合はTODOリストに書いておき、テストが通るようにする作業に戻る。
- テストを正しくする
- 色々と汚い手を使ってテストを作成したため、書いてしまった重複を削除することでテストを通るようにする。
テストをきれいに機能させる3つのアプローチ
- 仮実装:
失敗するテストを書いたあと、最初に行う実装は**「ベタ書きの値を返す」**ことである。これによりテストが通るようになれば、ベタ書きの値をだんだん本物の式や変数に置き換えていく。
以下の2つの要素によって、仮実装の有効性は説明できる。- 心理的効果:テストが通るのと通らないのでは精神状態が全く異なる。テストが通れば、自分が今どんな状態なのかを把握することができる。そこから自信をもってリファクタリングすることが可能である。
- スコープ制御:1つの実例から始め、そこから一般化を行うことで、本質と関係ない問題に気を取られずに作業を行える。つまり目の前の問題だけに集中することができる。
- 三角測量:
2つ以上の例があるときだけ一般化を行う。これにより、最も慎重に一般化を引き出すことが可能である。
例えば、足し算のテストで仮実装まで書いたとする。
public void testSum() {
@Test
public void testSum(){
assertEquals(4, plus(3,1));
}
private int plus(int augend, int addend) {
return 4;
}
}
正しい設計へと三角測量で導くには、以下のようにする
public void testSum() {
@Test
public void testSum(){
assertEquals(4, plus(3,1));
assertEquals(7, plus(3,4));
}
private int plus(int augend, int addend) {
return augend + addend;// 一般化する
}
}
2つ目の例が現れることで、plusメソッドを一般化する。
- 明白な実装:
シンプルな操作であればそのまま実装する。仮実装や三角測量はとても細かく刻んだステップである。シンプルな機能であれば実装をどうするべきか既に見えていることもある。その場合はそのまま実装した方が良い。もしテストに失敗してしまったら、仮実装や三角測量を用いて小さなステップで進めていく。