テスト駆動開発(TDD)は死んだって記事
正直、開発手法などの記事については上辺をすくっただけなので、よくわかりませんが、TDDは時代遅れの開発手法ということなのでしょうか?
しかし、 @t_wada さんのセミナーに参加して以来、TDDが気になって仕方ありませんでした。(ライブコーディングは手法の有用性を知らしめるのにいい手段ということもセミナーで思い知りました。)
当時の僕はかなりのレガシーコードを修正していて、この本に書かれている通り、リファクタリングをする際にかなりの不安をもって修正をしていたからです。
セミナーの内容を見よう見まねで、個人開発プロジェクトでTDDしていましたが、この際しっかり勉強しようということで、本を購入し、この記事にまとめたいと思います。
前提
この記事は テスト駆動開発 を読んで書いた記事ではありますが、下記のような注意点があります。
- この本の丸写しではない。つまり、僕の個人的価値観や経験をまじえて、僕が噛み砕いた内容を記載しています。
- TDDそのものの知識を得るためにはこの本を読むか、関連するTDDの本を見ることをお勧めします。
ここでの僕の価値観とは この記事 でも書いたように
- ユーザー>コード
- 速度→正確性の順で重要(正確性を軽視せよというわけではない)
と考えています。
もちろんこれに反論があるエンジニアの方もおられるかもしれませんが、これはTDDという開発手法ですので、合う合わないあると思いますし、これが正解とも言いません。
始めに
「TDDはテスト技法ではない。」これはまえがきにも書かれています。
TDDについて議論しているコミュニティを見ると、「自動テストは有用だが...」「テストを通すことが重要だ...」と書き込まれていることが少々あるが、これはTDDの議論ではないと僕は思います。
TDDとは分析技法であり、この分析を通じて開発をスムースに進める手法だと思っています。
だから、TDDの真価は「テストを書くことで製品の品質を担保するため」や、「テストを通すため」ではなく、「コーディングやリファクタリングで生じる欠陥と不安を出来るだけ排除するため」で、「品質の担保」や「テストのクリア」はTDDの副産物であると考えています。
簡単に言ってしまえば、 エンジニアの不安を取り除くための開発手法 ともいえるのではないでしょうか?
まえがきで、天才的なプログラマーにとってはこの手法はとてもまどろっこしく、遠回りで面倒とあります。
優秀なプログラマは最初から正しく綺麗で、確実に動くプログラムを書けるのですから、当然でしょう。
テストを最初に書くということは、「失敗を前提」にしているということなので、優秀なエンジニアにとってもは無用のものです。
しかし僕はとてもじゃありませんが、天才ではありませんし、僕の書いたコードでバグを作ってしまうということは沢山ありました。
凡庸なプログラマである僕にとってTDDは「安心できる」上に「着実に前進できる」手法であると思っています。
開発サイクル
とても有名な3つの単語を書きます。TDDのマントラだそうです。
- レッド:動作しない、おそらく最初のうちはコンパイルも通らないテスト1つ書く。
- グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
- リファクタリング:テストを通すために発生した重複をすべて除去する。
繰り返しになってしまいますが、凡庸な僕の頭の中で幼稚に翻訳した内容を書きます。
- 一番始めにテストコードを書く。このテストを通すプログラムはまだ書かれていないので当然テストはエラーで終了する。
- とりあえずテストを通すためのコードを書く。テストはクリアするが、実装が「とりあえず」なので、汚いコード
- 文字通り、リファクタリングする。汚い、つまりとりあえず書いたコードを綺麗な実装にする。
テストが意味するもの
TDDのテストとは、もちろんテストコードの事を指しますが、テストを書くことで、仕様チェックをしているとも受け取れます。
テストコードを書くことで、思いつかない様な仕様バグを発見出来ます。
テストを書くということは仕様を満たすコードを書くということにつながるので、「仕様をプログラムコード」にするという作業になります。
少しだけ毛色が違いますが、 このサイト のおっしゃることも腑に落ちました。
- レッド:仕様設計レビュー
- グリーン:探索テストレビュー
- リファクタリング:保守性レビュー
http://kokotatata.hatenablog.com/entry/2015/03/05/120318
から引用
僕はTDDは開発手法であるといいました。この方もアジャイル開発手法としてTDDを利用しているみたいでした。
歩幅
この本には歩幅という単語が頻繁に出てきます。
歩幅というのはサイクルの粒度を指します。
期待する値、仕様を満たすテストを先に書き、とりあえず実装し、綺麗にしていく。この粒度は個人や開発部分によって粒度を変えるてよいとあります。
当然、1発でクリアしてもいいですし、難しい所ではコードを変更するたびにチェックしていくのもいいです。
例えば、ボールの個数をユーザーに均等に割り振って、そのあまりを返すロジックがあったとします。
この時、テストはこのようになるでしょう。
/**
* test getRemainder method
*/
public function testGetRemainder()
{
$sample = new Sample(3, 2);
$this->assertEquals(1, $sample->getRemainder());
}
この実装は簡単なので、経験のある方ならすぐにゴールまで行けるでしょう。サイクルなど必要ありません。
class Sample
{
protected $ballNum;
protected $userNum;
public function __construct($ballNum, $userNum)
{
$this->$ballNum = $ballNum;
$this->$userNum = $userNum;
}
public function getRemainder()
{
return $ballNum % $userNum;
}
}
上記のようにすぐにゴールまで、たどり着くことができるでしょう。
簡単な実装なので、エンジニアは不安も抱きませんし、仕様も簡単なので、答え合わせも簡単です。
この状態が歩幅が大きいと言えます。ゴールまで1歩でクリアしていくということですね。
仮にこの処理が実装の難しい箇所と想定したときの話をしましょう。
実装が難しく、エンジニアは不安を抱えています。仕様も理解しやすいとは言い難く、所謂「仕様がふわふわしている」状態です。
ここの歩幅は細かくするといいでしょう。
サイクルを頻繁に回す、つまり歩幅を小さく細かく実装していくことで、チェックを頻繁にし、エンジニアの不安を解消し、仕様の理解を助けます。
/**
* test getBallNumPerUser method
*/
public function testGetBallNumPerUser()
{
$sample = new Sample();
$this->assertEquals(1, $sample->getRemainder());
}
とりあえずテストを実行してみましょう。
この状態でテストを流すと、Sample
クラスもgetRemainder
メソッドも存在しないので、エラーが発生します。
これがレッドですね。
これをすぐに解消するために、中身を実装しましょう。
一歩目は下記のように実装しました。
class Sample
{
public function getRemainder()
{
return 1;
}
}
テストクリアです。
とりあえずプログラムは動作します。
しかし、こんなプログラムに意味はありません。
有用な処理にするためにリファクタリングしましょう。
ボールを均等に配布した結果、あまりの数を出力するのですから、ボールの全ての個数をユーザー数で割り算し、そのあまりを出力すればいいですね。
class Sample
{
public function getRemainder()
{
return 3 % 2;
}
}
テストクリアです。
とりあえずプログラムは動作します。
しかしこれではボールの数が3つ固定で、ユーザーの全体数も2で固定されています。
これも有用とは言えません。
もう一度リファクタリングしましょう。
ボール数とユーザー数を格納する変数を用意したらいいですね。
class Sample
{
protected $ballNum = 3;
protected $userNum = 2;
public function getRemainder()
{
return $this->ballNum % $this->userNum;
}
}
テストクリアです。
しかしこれでは固定値の問題をクリアできていません。
コンストラクタで値を設定してあげましょう。
class Sample
{
protected $ballNum;
protected $userNum;
public funcion __construct($ballNum, $userNum)
{
$this->ballNum = $ballNum;
$this->userNum = $userNum;
}
public function getRemainder()
{
return $this->ballNum % $this->userNum;
}
}
テストクリアです。
可変数で計算結果を取得できるようにもなりましたし、リファクタリングはここまでとしておきましょう。
(もちろん、入出力バリデーションなど、バグを生む実装ですが、ここではあえて触れていません。気になる方はTDDで続きを実装しても面白いかもしれませんね。)
これで最初に書いた実装と同じ実装になりましたね。
ここで何が言いたいかというと、
- 個人の技量
- 設計の複雑さ
- 実装時間の制約
さまざまな理由によって歩幅は変えることができるということです。
TDDが受け入れがたい方の意見の1つにこんなものがあります。
なにか実装を変更するたびに、後ろを振り返ってチェックする。
しかも大量に実装されるであろうテストコードをだ。
これは時間の無駄だし、効率が悪い。
僕もそう思います。
大きなプロジェクトになってくるとテストを流すだけで、何十分、何時間と取られますし、明らかにクリアできるコードをテストするのは効率が悪いです。
しかし、TDDは歩幅の調節ができます。
明らかに無駄、明らかにテストクリアできる実装だとわかっている場合は歩幅を大きく取ってしまえばいいのです。
TDDはエンジニアの不安を取り除く開発手法です。
テストをクリアするためのものではありませんし、品質を担保するものでもありません。
不安がなければテストしなければいいのです。
不安が出てくれば歩幅を小さくとって、何回も振り返り、チェックをしましょう。
テストと「対話」する
TDDとの大きなメリットを感じる1つの考え方です。
これは本に書いてあったか忘れてしまいましたが、僕はTDDはテストと「対話」する開発手法だと考えています。
不安を感じたらテストと相談する
プロジェクトに限らず、何かに不安を感じたら、人は他人に相談することでしょう。
それと同じように、仕様や実装に不安を感じたらテストに聞いてみましょう。
テストコードの実装に苦労したり、テストコードの実装中に明らかに仕様バグを生む設計である可能性を消すことができます。
テストを一度書いたらそれでFixでは無い
前章で「歩幅」について書きました。テストコードを書いて、実装します。このサイクルが細かいというのは、機能毎という意味もありますが、1機能単位でもありえます。
- テストコードを書き実装した結果、 テスト内容が未熟であることに気づく
- 該当箇所のテストコードを修正している間、該当箇所の仕様バグを発見
- 仕様バグを修正する実装
・・・
僕がTDDで実装していると、上記の感じで実装の進捗とともにテストコードもだんだん良くなっていく現象がありました。
この現象がとても良く、テストコード実装=仕様確認をしている時間となります。
設計書とにらめっこしているよりもよほど有意義な時間となりました。
実際の工数について
工数は明らかに増える
工数は明らかに増えます。2倍くらいになります。当然です。テストコードは仕様の実装確認を示すコードですから実装分だけテストコードも増えます。
しかし、体感工数は少なくなります。
体感工数=ストレス
体感工数とはストレスだと考えています。
明らかに物理時間が伸びているのに何故かというと、「出戻り」が格段に減っています。
仕様確認をテストコードを書く時間に行っているため、
実装する前に仕様的なバグなどに気づくことができます
従来の方法で実装していると、実装してから気付いて、また実装・・・という悪循環です。
TDDの場合は、それは限りなく少なくなるので、実装時間が伸びますが、その分「確実に進捗が出ます」。
これはエンジニアのメンタルに非常に優しいです。
まさに「エンジニアの不安を取り除くための開発手法」といえます。
最後に
最後の方は少し駆け足気味になってしまいましたが、僕のTDDに対する感情をつらつらと述べてみました。
実務的な記事も今後書いていこうと思いますが、正直
個人によってTDDが合う、合わないはある
と思います。
皆さんも一度お試しになって、実務で導入するかどうかを決めたほうが良いかもしれませんね。
.