Prev Next

第14章 振舞駆動開発

[Astels2006] において、Dave Astels は次のように述べています。

  • エクストリーム・プログラミング (日本語) は本来、壊れる可能性のあるものはすべてテストするという決まりがあった。

  • 今ではしかし、エクストリーム・プログラミングにおけるテスト手法は テスト駆動開発 (日本語) に進化した (第 13 章 を参照ください)。

  • しかし、各種ツールは未だにテストの語彙で考えることを強要し、 スペックではなくアサーションで考えさせようとする。

 

So if it's not about testing, what's it about? (テストじゃないっていうけど、じゃあいったい何なの?)

It's about figuring out what you are trying to do before you run off half-cocked to try to do it. You write a specification that nails down a small aspect of behaviour in a concise, unambiguous, and executable form. It's that simple. Does that mean you write tests? No. It means you write specifications of what your code will have to do. It means you specify the behaviour of your code ahead of time. But not far ahead of time. In fact, just before you write the code is best because that's when you have as much information at hand as you will up to that point. Like well done TDD, you work in tiny increments... specifying one small aspect of behaviour at a time, then implementing it.

- あなたがこれから何をしようとしているのかを事前にきちんと把握することで、 準備不足のまま逃げ出してしまうようなはめにならないようにするものです。 あなたが書くスペックは、ある振る舞いのちょっとした側面を 簡潔で明確かつ実行可能な形式で表したものとなります。 ただそれだけの簡単なこと。 え?それってテストじゃないのかって? そう、テストではないのです。 あなたは「そのコードがどう動くべきか」という仕様 (スペック) を書くのです。実際のコードを書く前にコードの振る舞いを定義することになります。 とはいえ、それはコードを書くずっと前にということではありません。 実際のところは、コードを書く直前にスペックを書くのがよいでしょう。 コードを書く際に利用する情報とスペックを書く際に利用する情報がほぼ同じになるからです。 TDD のときと同様、小さい作業の積み重ねで進めていきます。 一度に定義する振る舞いは小さなものにとどめ、 その単位で実装を進めていくのです。

When you realize that it's all about specifying behaviour and not writing tests, your point of view shifts. Suddenly the idea of having a Test class for each of your production classes is ridiculously limiting. And the thought of testing each of your methods with its own test method (in a 1-1 relationship) will be laughable.

- 仕様を定義することとテストをかくことの違いを理解すれば、ものの見方が変わります。 実装クラスのひとつひとつに対応するテストクラスを作成するなどという考え方が おそろしく窮屈なものに見えてくることでしょう。 個々のメソッドにそれぞれテストメソッドを (1 対 1 対応で) 用意するなんてばかばかしくなってきます。

 
  --Dave Astels

振舞駆動開発 (日本語) が注目するのは、ソフトウェア開発の際に使用する言語やインタラクションです。 振舞駆動開発では、よく目にする ドメイン駆動設計 の語彙を用いてコードの目的や利点を記述します。 これにより、開発者が技術的な詳細よりも「なぜそのコードを書かなければいけないのか」 に注目できるようになります。そして、 コードを書くときに使う言語とドメインエキスパートが話す用語との間の翻訳の手間を最小限にできます。

PHPUnit_Extensions_Story_TestCase クラスはストーリーフレームワークを提供します。 これは、振舞駆動開発のための ドメイン特化言語 (日本語) の定義を支援するものです。 シナリオ (scenario) の中において、 given()when() そして then()ステップ (step) を表します。 and() は直前のステップと同じ種類のものを表します。 次のメソッドが PHPUnit_Extensions_Story_TestCaseabstract として宣言されており、 これらを実装する必要があります。

  • runGiven(&$world, $action, $arguments)

    ...

  • runWhen(&$world, $action, $arguments)

    ...

  • runThen(&$world, $action, $arguments)

    ...

ボウリングゲームの例

この節では、ボウリングゲームのスコアを計算するクラスの例を見てみましょう。 ボウリングのルールは次のとおりです。

  • ひとつのゲームは 10 フレームで構成される

  • 10 本のピンを倒すため、各フレームでプレイヤーは 2 回投げることができる

  • 各フレームのスコアは倒したピンの総数で、ストライクやスペアの際にはさらにボーナスが追加される

  • スペアとは、2 回投げて 10 本のピンをすべて倒すこと

    その場合のボーナスは、次に投げたときに倒したピンの数

  • ストライクとは、1 投目で 10 本のピンをすべて倒すこと

    その場合のボーナスは、次の 2 投で倒したピンの数

例 14.1 は、上にまとめたルールを PHPUnit_Extensions_Story_TestCase でスペックシナリオとして書き下ろしたものです。

例 14.1: BowlingGame クラスのスペック

<?php
require_once 'PHPUnit/Extensions/Story/TestCase.php';
require_once 'BowlingGame.php';

class BowlingGameSpec extends PHPUnit_Extensions_Story_TestCase
{
/**
* @scenario
*/
public function scoreForGutterGameIs0()
{
$this->given('New game')
->then('Score should be', 0);
}

/**
* @scenario
*/
public function scoreForAllOnesIs20()
{
$this->given('New game')
->when('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->then('Score should be', 20);
}

/**
* @scenario
*/
public function scoreForOneSpareAnd3Is16()
{
$this->given('New game')
->when('Player rolls', 5)
->and('Player rolls', 5)
->and('Player rolls', 3)
->then('Score should be', 16);
}

/**
* @scenario
*/
public function scoreForOneStrikeAnd3And4Is24()
{
$this->given('New game')
->when('Player rolls', 10)
->and('Player rolls', 3)
->and('Player rolls', 4)
->then('Score should be', 24);
}

/**
* @scenario
*/
public function scoreForPerfectGameIs300()
{
$this->given('New game')
->when('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->then('Score should be', 300);
}

public function runGiven(&$world, $action, $arguments)
{
switch($action) {
case 'New game': {
$world['game'] = new BowlingGame;
$world['rolls'] = 0;
}
break;

default: {
return $this->notImplemented($action);
}
}
}

public function runWhen(&$world, $action, $arguments)
{
switch($action) {
case 'Player rolls': {
$world['game']->roll($arguments[0]);
$world['rolls']++;
}
break;

default: {
return $this->notImplemented($action);
}
}
}

public function runThen(&$world, $action, $arguments)
{
switch($action) {
case 'Score should be': {
for ($i = $world['rolls']; $i < 20; $i++) {
$world['game']->roll(0);
}

$this->assertEquals($arguments[0], $world['game']->score());
}
break;

default: {
return $this->notImplemented($action);
}
}
}
}
?>
phpunit --story BowlingGameSpec
PHPUnit 3.4.14 by Sebastian Bergmann.

BowlingGameSpec
 [x] Score for gutter game is 0

   Given New game
    Then Score should be 0

 [x] Score for all ones is 20

   Given New game
    When Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
    Then Score should be 20

 [x] Score for one spare and 3 is 16

   Given New game
    When Player rolls 5
     and Player rolls 5
     and Player rolls 3
    Then Score should be 16

 [x] Score for one strike and 3 and 4 is 24

   Given New game
    When Player rolls 10
     and Player rolls 3
     and Player rolls 4
    Then Score should be 24

 [x] Score for perfect game is 300

   Given New game
    When Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
    Then Score should be 300

Scenarios: 5, Failed: 0, Skipped: 0, Incomplete: 0.

Prev Next
1. 自動テスト
2. PHPUnit の目標
3. PHPUnit のインストール
4. PHPUnit 用のテストの書き方
テストの依存性
データプロバイダ
例外のテスト
PHP のエラーのテスト
5. コマンドラインのテストランナー
6. Fixtures
tearDown() よりも setUp()
バリエーション
Fixture の共有
グローバルな状態
7. テストの構成
ファイルシステムを用いたテストスイートの構成
XML 設定ファイルを用いたテストスイートの構成
テストケースクラスの使用法
8. テストケースの拡張
出力内容のテスト
9. データベースのテスト
データセット
Flat XML データセット
XML データセット
CSV Data Set
データセットの交換
操作
データベースのテストのコツ
10. 不完全なテスト・テストの省略
不完全なテスト
テストの省略
11. テストダブル
スタブ
モックオブジェクト
ウェブサービスのスタブおよびモック
ファイルシステムのモック
12. テストの進め方
開発中のテスト
デバッグ中のテスト
13. テスト駆動開発
銀行口座の例
14. 振舞駆動開発
ボウリングゲームの例
15. コードカバレッジ解析
カバーするメソッドの指定
コードブロックの無視
ファイルのインクルードや除外
16. テストのその他の使用法
アジャイルな文書作成
複数チームでのテスト
17. 雛形ジェネレータ
テストケースクラスの雛形の作成
テストケースクラスからのクラスの雛形の作成
18. PHPUnit と Selenium
Selenium RC
PHPUnit_Extensions_SeleniumTestCase
19. ログ出力
テスト結果 (XML)
テスト結果 (TAP)
テスト結果 (JSON)
コードカバレッジ (XML)
20. ビルドの自動化
Apache Ant
Apache Maven
Phing
21. 継続的インテグレーション
Atlassian Bamboo
CruiseControl
phpUnderControl
22. PHPUnit API
概要
PHPUnit_Framework_Assert
assertArrayHasKey()
assertClassHasAttribute()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertGreaterThan()
assertGreaterThanOrEqual()
assertLessThan()
assertLessThanOrEqual()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertSame()
assertSelectCount()
assertSelectEquals()
assertSelectRegExp()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertTag()
assertThat()
assertTrue()
assertType()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
PHPUnit_Framework_Test
PHPUnit_Framework_TestCase
PHPUnit_Framework_TestSuite
PHPUnit_Framework_TestResult
パッケージの構成
23. PHPUnit の拡張
PHPUnit_Framework_TestCase のサブクラスの作成
アサートクラスの作成
PHPUnit_Extensions_TestDecorator のサブクラスの作成
PHPUnit_Framework_Test の実装
PHPUnit_Framework_TestResult のサブクラスの作成
PHPUnit_Framework_TestListener の実装
新しいテストランナーの作成
A. アサーション
B. アノテーション
@assert
@backupGlobals
@backupStaticAttributes
@covers
@dataProvider
@depends
@expectedException
@group
@outputBuffering
@runTestsInSeparateProcesses
@runInSeparateProcess
@test
@testdox
@ticket
C. XML 設定ファイル
PHPUnit
テストスイート
グループ
コードカバレッジ対象のファイルの追加や除外
ログ出力
テストリスナー
PHP INI 項目や定数、グローバル変数の設定
Selenium RC の設定ブラウザ
D. 目次
E. 参考文献
F. 著作権