2011年8月21日に開催されたTDD BOOT CAMP 1.7 for PHPに参加してきました。
当日は @t_wada さんの基調講演の後、LTを経て午後からペアプロによるTDDの演習が行われました。
本記事では演習のお題1のコードを書きながら感じたこと学んだことについて、実際に書いたコードを例に紹介します。
目次
演習お題
- お題1:Wikiエンジン
- お題2:ショッピングカート
TDDにおいて意識すること
テストの定義を明確に
TDDにおけるテストの定義は、開発者が自分たちで開発をすすめるためのテスト。
これは日本でよく言われる単体テスト、受け入れテストではない。
TDDの進め方
- テストを書き
- そのテストを失敗させ(red)
- 目的のコードを書き
- テストを成功させる(green)
- ファクタリングを行う
- 書いたテストをまずは失敗させる。いきなり成功するテストコードを書いてしまうと、その後の失敗を補足できない可能性がある。
- テストケースをすべて書いてからコードを書き始めない。redな状態が続き、問題の判別が難しくなる。
- 目的のコードを書いてredからgreenに持っていくときは、コードが汚くても動いていれば良い。仕様通り動いているかどうかを規定するのは1で書いたテストコード。
- greenになったところでリファクタリングしてコードを綺麗に。コードを綺麗にするのはリファクタリングだけ。
- リファクタリングをしなければテスト駆動開発ではない。
少しずつ、1つずつ進める
上記5ステップを少しずつ、ひとつずつこなしていく。
小さなコードで素早く問題に近づき、即座にフィードバックを得て問題の解決にあたっていく。
どこをテストするか
- コードを書いていて不安な場所をテストする。自分に問いながらテストコードを書く。
- テストの粒度にはムラが生まれてしまってもよい。
- 大事なのはカバレージを満たすことではない。
なぜTDDをするか
- 最大の理由は開発者が安心してコードを書くため。
- リファクタリングをしてコードを綺麗にするためには開発者が安心してコードを書く必要がある。
リファクタリングして新しく書いたコードが過去に書いたコードとの結果が変わっていないことをテストコードにより保証することで安心が生まれる。 - もちろんソフトウェア工学的なメリットはある。モジュールの依存関係を排除した構造になっていく。
演習1実践記
一人で進めているように書いていますが、実際にはペアを組んでくださった方と話し合いながら進めました。
可能な限り、当日の流れと思考を再現しています。
当日のコードをバージョン管理しておけばよかった。
実践記の概要
実践記の流れは下記のとおりです。
- テストを書く。
- テストを失敗させる。
- テストを成功させる。
- 別の視点からテストを書き、テストを失敗させる。
- テストを成功させる。目的のコードに近づけていく。
- リファクタリングする。
テストを書く。
WikiEngineTest.php
class WikiEngineTest extends PHPUnit_Framework_TestCase {
public function testイコールで囲まれていたらh1タグに変換() {
$this->assertequals('<h1>HeadLine</h1>', $this->perseEqual('= HeadLine ='));
}
}
上記は「= HeadLine =」という文字列をメソッドの引数に与えた場合、「<h1>HeadLine</h1>」という文字列が返ってくることを期待するテストです。
テストでは仕様を満たすことを意識します。
初期の段階で仕様を満たすテストケースをすべて並べてしまうのは非推奨のようです。テストが失敗する状態が長く続いてしまいます。
TDDBCでは、最初のうちはテストコードの中に実装を書き、ある程度実装が固まってから実装を本体クラスに移す方法を推奨していました。演習でもテストクラスに実装を書きました。
テストを失敗させる。
<?php
class WikiEngineTest extends PHPUnit_Framework_TestCase {
public function testイコールで囲まれていたらh1タグに変換() {
$this->assertequals('<h1>HeadLine</h1>', $this->perseEqual('= HeadLine ='));
}
public function perseEqual($value) {
return '';
}
}
$ phpunit WikiEngineTest.php
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
テストを失敗させる最小の実装として、preseEqualメソッドが常に空文字を返すようにしました。
TDDではテストをいきなりgreenにせず、まずはredの状態にします。
失敗する状態をきちんと確認するためです。
テストを成功させる。
class WikiEngineTest extends PHPUnit_Framework_TestCase {
public function testイコールで囲まれていたらh1タグに変換() {
$this->assertequals('<h1>HeadLine</h1>', $this->perseEqual('= HeadLine ='));
}
public function perseEqual($value) {
return '<h1>HeadLine</h1>';
}
}
$ phpunit WikiEngineTest.php
OK (1 test, 1 assertion)
最も簡単に成功させる方法として、テストが期待する結果をそのまま返す方法を選択しました。
もちろんこの実装では問題があるので、次で書くテストで実装の問題点を明らかにします。
別の視点からテストを書き、テストを失敗させる。
<?php
class WikiEngineTest extends PHPUnit_Framework_TestCase {
public function testイコールで囲まれていたらh1タグに変換() {
$this->assertequals('<h1>HeadLine</h1>', $this->perseEqual('= HeadLine ='));
$this->assertequals('<h1>Title</h1>', $this->perseEqual('= Title ='));
}
public function perseEqual($value) {
return '<h1>HeadLine</h1>';
}
}
$ phpunit WikiEngineTest.php
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.
「= Title =」という文字列を渡したら「<h1>Title</h1>」という文字列が返ってくることを期待するテストを追記しました。テストは失敗しています。
テストを成功させる。目的のコードに近づけていく。
<?php
class WikiEngineTest extends PHPUnit_Framework_TestCase {
public function testイコールで囲まれていたらh1タグに変換() {
$this->assertequals('<h1>HeadLine</h1>', $this->perseEqual('= HeadLine ='));
$this->assertequals('<h1>Title</h1>', $this->perseEqual('= Title ='));
}
public function perseEqual($value) {
$regex = '/^= (.+) =$/';
if (preg_match($regex, $value, $matches)) {
return '<h1>'. $matches[1] .'</h1>';
}
return $value;
}
}
$ phpunit WikiEngineTest.php
OK (1 test, 2 assertions)
今度は正規表現を使って様々な文字を入力されても正しく動くように実装します。
多少コードが冗長でもこの時点では動いていれば良いです。リファクタリングは後で検討します。
不安な部分をテストする
public function testイコールで囲まれていたらh1タグに変換() {
$this->assertequals('<h1>HeadLine</h1>', $this->perseEqual('= HeadLine ='));
$this->assertequals('<h1>Title</h1>', $this->perseEqual('= Title ='));
$this->assertequals('<h1> Title</h1>', $this->perseEqual('= Title ='));
$this->assertequals('<h1>タイトル</h1>', $this->perseEqual('= タイトル ='));
}
public function testイコールで囲まれていなければ与えられた文字列を返す() {
$this->assertequals('title', $this->perseEqual('title'));
}
$ phpunit WikiEngineTest.php
OK (2 test, 5 assertions)
実装に不安があったのでテストを追記しました。
- 行頭にスペースを入れてみたらどうなるか?
- マルチバイト文字を入れてみたらどうなるか?
- イコールで囲まれていなかったらどうなるか?
テストは通っています。もう少しいろんなケースを試してみたいですが、先に進みました。
テストがgreenな状態なのでこのあたりでリファクタリングを検討してみます。
リファクタリングする。
<?php
class WikiEngineTest extends PHPUnit_Framework_TestCase {
/**
* @dataProvider stringTypesProvider
*/
public function testイコールで囲まれていたらh1タグに変換($expected, $actual) {
$this->assertequals($expected, $this->perseEqual($actual));
}
public function stringTypesProvider() {
return array(
array('<h1>Headline</h1>', '= Headline ='),
array('<h1> Title</h1>', '= Title ='),
array('<h1> タイトル</h1>', '= タイトル ='),
);
}
public function testイコールで囲まれていなければ与えられた文字列を返す() {
$this->assertequals('title', $this->perseEqual('title'));
}
public function perseEqual($value) {
$regex = '/^= (.+) =$/';
if (preg_match($regex, $value, $matches)) {
return '<h1>'. $matches[1] .'</h1>';
}
return $value;
}
}
assertequalsメソッドがパラメータ毎にズラズラ並んでいたので、PHPUnitのdataProviderという仕組みを利用してリファクタリングを行いました。
以上のステップを繰り返す。
このあとも同じようにred, green, リファクタリングのステップを繰り返しながら進めました。
私たちのペアはExceptionを捕獲する課題が終わったところで時間切れとなりました。
実践記のステップがあまりに細かすぎると感じた方もいらっしゃるかもしれません。これについてはテスト駆動開発に慣れてくると徐々に最適化されてくるのではないかと思います。
初TDDの感想
- レッド、グリーン、リファクタリングの黄金の回転は非常に心地がよく、着実に前にすすんでいることを実感できました。
- ふと気づくと動作確認をしていないことに驚きました。
作成した関数の戻り値をprint文で出力したり、デバッガを立ち上げて変数の状況を確認してみたり。 - リファクタリングを行って大きくコードを変えても、テストコードが通っている=仕様を満たせている、という安心感がありました。
初ペアプロの感想
- コーディング中の選択ひとつひとつをペアの方と議論して進められるため、自然と質が上がると思います。
- トラブル対応などで一刻も早く対応せねばならず、冷や汗を書きながら震える手でコーディングせねばならない状況においてもペアプロは有効ではないでしょうか。
- 他人のコーディングをライブで見ることは気づきの宝庫です。
- メリットも多いけど、業務で毎日ペアプロやるのはしんどそう。週1でペアプロデーを設けるくらいがちょうどいいかも。
勉強会全体の感想
お弁当が出て、みんなで食事をとったのはよかったです。食事中に音楽が流れており、リラックスできたのではないでしょうか。隠れたアイスブレイクだったのではないかと思います。
全体的に和やかな雰囲気で進み、出席者同士の会話も盛んに行われていました。スタッフの方々の細かな気遣い、場づくりの努力があってこそ。
スタッフの方々、参加された皆様のおかげでよい勉強会になりました。