テスト駆動開発をやってみる

テスト駆動開発って僕なりに都市伝説的なところがあったのでやってみた。
サンプルとしての機能は、入力情報をテーブルに格納する部分(モデル)にしてみた

###アプリ側の作成
テスト駆動開発というと「テストから書いていくもの」と認識しておりまして、classもファイルも何も書かずにテストを先にと書くと思っておりましたが、それだと「テストを書いたとしてもテストを実行する」ことが出来なかったので、メソッド名くらいまでは作ることにしました。

```php:Register.php
<?php
namespace Model\Drink\Register
{
    class Constant
    {
        const SQL_REGISTER_DRINK = <<<EOF
INSERT INTO `member_drink` (
`member_id`,
`drink`,
`created`)
VALUES (
:member_id,
:drink,
:created)
EOF;
    }
}

namespace Model\Drink
{
    use Model\Base;

    interface iRegister
    {
        public function insertDrinkData( array $args );
    }
    
    class Register extends Base implements iRegister
    {
        public function insertDrinkData( array $args )
        {
        }
    }
}
```

ということで、上記の「insertDrinkData」メソッドについてのテストを書くことにする

###テストの作成：大枠を書く
PHPUnitのテンプレートにそって作成

```php:RegisterTest.php
<?php
namespace Model\Drink;

class RegisterTest extends \PHPUnit_Framework_TestCase
{
}

```

###テストの作成：モックを書く
このメソッドは引数を受け取ってそれをテーブルに格納するのですが、テストでは実際のテーブルには値を入れたくないのでDBAのモックを作成してテーブルにデータを格納せずともテストが通るようにします

```php:RegisterTest.php
<?php
namespace Model\Drink;

class RegisterTest extends \PHPUnit_Framework_TestCase
{
    function stubPDO ()
    {
        $prepareStub = $this->getMockBuilder('PrepareStub')
            ->setMethods([
                'execute',
            ])
            ->getMock();

        $pdoStub = $this->getMockBuilder('PdoStub')
            ->setMethods([
                'beginTransaction',
                'prepare',
                'commit',
                'fetchOne',
            ])
            ->getMock();

        $pdoStub->method('prepare')->willReturn($prepareStub);

        return $pdoStub;
    }
}
```

###テストの作成：正常ケース
アプリ側「insertDrinkData」メソッドについて考えます。
　・引数の配列には、member_idとdrinkが入っている
　・DBの登録が正常に終了したらtrueを返す

上記を想定して正常テストを作成する

```php:RegisterTest.php
...

class RegisterTest extends \PHPUnit_Framework_TestCase
{
    function testValidInsertDrink()
    {
        $stubPDO  = $this->stubPDO();
        $register = new Register( new ConnectionLocator(), $stubPDO);

        $res = $register->insertDrinkData([
            'memberID' => '12345667890',
            'drink'    => 1,
        ]);

        $this->assertTrue($res);
    }

    function stubPDO ()
    {...}
}

```

###テストを実行する
テストを実行してエラーになるかを確認します

```
$ vendor/bin/phpunit test/Model/Drink/RegisterTest.php
PHPUnit 4.2.6 by Sebastian Bergmann.

F

Time: 59 ms, Memory: 1.50Mb

There was 1 failure:

1) Model\Drink\RegisterTest::testValidInsertDrink
Failed asserting that null is true.

Model/Drink/RegisterTest.php:29
```

trueとなるところがnullで失敗すること（F）を確認できました

###テストが通るようにアプリ側を修正します
引数を受け取って、DBAを利用してテーブルにデータを格納するように書き足します。
最後に、trueを返すようにします。
※DBAで問題が発生した際はDBA側で例外が起きるようにしております

```php:Register.php
...

namespace Model\Drink
{
    use Model\Base;
    use Model\Drink\Register\Constant;

    interface iRegister
    {
        public function insertDrinkData( array $args );
    }

    class Register extends Base implements iRegister
    {
        public function insertDrinkData( array $args )
        {
            $member_ID = $args['memberID'];
            $drink     = $args['drink'];
            $created   = $this->dateTime();

            $this->registerTransaction( $this->sqlInsertDrink(), [
                'member_id' => $member_ID,
                'drink'     => $drink,
                'created'   => $created,
            ]);

            return true;
        }

        private function sqlInsertDrink ()
        {
            return Constant::SQL_REGISTER_DRINK;
        }
    }
}
```

###テストが通ることを確認する

```
$ vendor/bin/phpunit test/Model/Drink/RegisterTest.php
PHPUnit 4.2.6 by Sebastian Bergmann.

.

Time: 60 ms, Memory: 1.75Mb

OK (1 test, 1 assertion)
```

テストが通ること（.）を確認しました

###例外を考える
次に例外を起こすケースを考えます。

```
・引数のmember_idはString形式である
・引数のdrinkはint形式である
上記の条件以外は例外が起きるようにします
```

###例外が起きるテストを書く
「member_idをint形式で渡す」「drinkを文字列で渡す」２つのテスの作成

```php:RegisterTest.php
...

    /**
     * @expectedException UnexpectedValueException
     */
    function testInvalidInsertDrinkExceptionMemberID ()
    {
        $stubPDO  = $this->stubPDO();
        $register = new Register( new ConnectionLocator(), $stubPDO);

        $res = $register->insertDrinkData([
            'memberID' => 100,
            'drink'    => 1,
        ]);
    }

    /**
     * @expectedException UnexpectedValueException
     */
    function testInvalidInsertDrinkExceptionDrink ()
    {
        $stubPDO  = $this->stubPDO();
        $register = new Register( new ConnectionLocator(), $stubPDO);

        $res = $register->insertDrinkData([
            'memberID' => '1234567890',
            'drink'    => 'foo',
        ]);
    }

...
```

### テストを実行する
テストが失敗することを確認する

```
$ vendor/bin/phpunit test/Model/Drink/RegisterTest.php
PHPUnit 4.2.6 by Sebastian Bergmann.

.FF

Time: 52 ms, Memory: 1.75Mb

There were 2 failures:

1) Model\Drink\RegisterTest::testInvalidInsertDrinkExceptionMemberID
Failed asserting that exception of type "UnexpectedValueException" is thrown.

2) Model\Drink\RegisterTest::testInvalidInsertDrinkExceptionDrink
Failed asserting that exception of type "UnexpectedValueException" is thrown.

FAILURES!
Tests: 3, Assertions: 3, Failures: 2.
```

###テストが通るように修正
引数の形式をチェックするロジックをアプリ側に追加する

```php:Register.php
...
        public function insertDrinkData( array $args )
        {
            new \SplString($args['memberID']);
            new \SplInt($args['drink']);

            $member_ID = $args['memberID'];
            $drink     = $args['drink'];
            $created   = $this->dateTime();

            $this->registerTransaction( $this->sqlInsertDrink(), [
                'member_id' => $member_ID,
                'drink'     => $drink,
                'created'   => $created,
            ]);

            return true;
        }
...
```

###テストを実行する

```
$ vendor/bin/phpunit test/Model/Drink/RegisterTest.php
PHPUnit 4.2.6 by Sebastian Bergmann.

...

Time: 49 ms, Memory: 1.75Mb

OK (3 tests, 3 assertions)
```

###所感

とりあえず、テスト駆動開発的な感じでアプリを作成してみました。
ロジックを