slimでphpunit使用してユニットテスト

今回はphpunitをインストールした上で、ユニットテストを行う手順を記します。
ユニットテストはメソッド単体だったりの小さい粒度のテストで
今回はsrc/Taka512/Services/NameService.phpのgetNameメソッドをテストします。
サービスクラスは以下のような感じでコンテナに登録しているとします。

$container['app.services.name_service'] = $container->share(function ($c) {
    return new \Taka512\Services\NameService($c);
});

まずアプリケーション側をテストしやすい形に作り替えます。
具体的にはindex.phpで行っていたコンテナ作成処理等をbootstrap.phpに移動します。

web/index.php

<?php
define('ENVIRONMENT', 'prod');
require '../config/bootstrap.php';

$container = createContainer();
$app = $container['app'];
require PROJECT_DIR.'/config/routes.php';
$app->run();

config/bootstrap.php

<?php
define('PROJECT_DIR', dirname(__FILE__) . '/..');
require PROJECT_DIR . '/vendor/autoload.php';

function createContainer()
{
    require PROJECT_DIR . '/config/config.php';
    $container = new \Pimple();
    $container['app'] = new \Slim\Slim($config);

    require PROJECT_DIR . '/config/parameters.php';
    require PROJECT_DIR . '/config/databases.php';
    require PROJECT_DIR . '/config/services.php';

    return $container;
}

phpunitのインストール

composerでインストールするとvendor/binの下にphpunitがインストールされます。

$ vi composer.json
    "require": {
        "slim/slim":       "2.*",
        "slim/extras":     "2.0.*",
        "twig/twig":       "1.*",
        "pimple/pimple":   "v1.0.2",
        "phpunit/phpunit": "3.7.21",
    },
$ php composer.phar update
$ ls vendor/bin/phpunit
vendor/bin/phpunit

テストの設定ファイルを作る

ユニットテスト用の設定ファイルをconfigの下に作成します。
先ほど作成したbootstrapを読み込んで「tests/unit」下のテストを起動する設定となります。

$ vi config/unit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap = "bootstrap.php" >
    <testsuites>
        <testsuite name="unit test">
            <directory>../tests/unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

テスト作成

テストディレクトリを掘ってテストファイルを作成します。

$ mkdir -p test/unit/Services
$ vi tests/unit/Services/NameServiceTest.php
<?php
namespace Taka512\Services;

class NameServiceTest extends \PHPUnit_Framework_TestCase
{
    public function testGetName()
    {
        \Slim\Environment::mock();
        $container = createContainer();
        $name = $container['app.services.name_service']->getName();
        $this->assertEquals($name, 'hoge');
    }
}


テスト実行

設定ファイルを指定してphpunitを実行するとサービスクラスのテストができましたとさ!

$ vendor/bin/phpunit -c config/unit.xml
PHPUnit 3.7.21 by Sebastian Bergmann.

Configuration read from /home/coh2/config/unit.xml

.

Time: 0 seconds, Memory: 5.00Mb

OK (1 test, 1 assertion)

slimでpimpleを使ってデータベースに接続

pimpleを使ってデータベースへの接続を行いデータベースからデータを取ってくるようにします。

データベースにテーブルとデータを追加

MYSQLのhogeデータベースにユーザ「hoge」パスワード「hogepass」で接続できるように設定してます。

create databases hoge;
GRANT ALL PRIVILEGES ON hoge.* TO hoge@localhost IDENTIFIED BY 'hogepass';
flush privileges;
mysql> use hoge
mysql> create table user (name varchar(10));
mysql> insert into user (name)values('taka512');

dbに接続するクラスを作成

PDOを利用してMYSQLに接続するクラスです。

$ mkdir src/Taka512/Db
$ vi src/Taka512/Db/Mysql.php
<?php
namespace Taka512\Db;
class Mysql
{
    protected static $conn = null;
    public static function connect($host, $database, $user, $password)
    {
        if (self::$conn == null) {
            try {
                self::$conn = new \PDO(
                    sprintf('mysql:host=%s;dbname=%s;charset=utf8', $host, $database),
                    $user,
                    $password,
                    array(\PDO::ATTR_EMULATE_PREPARES => false)
                );
            } catch (\PDOException $e) {
                echo 'Connection failed: ' . $e->getMessage();
                die;
            }
        }
        return self::$conn;
    }
}

dbからデータを取得処理を作成

今までgetNameは固定文字列を返してましたがデータベースからデータを取得するように修正

$ vi src/Taka512/Services/NameService.php
class NameService
{
~省略~
    public function getName()
    {
        $name = null;
        try {
            $stmt = $this->di['db_master']->query("SELECT name FROM user");

            while($row = $stmt->fetch(\PDO::FETCH_ASSOC)){
                $name =  $row['name'];
            }
        } catch (\PDOException $e){
            var_dump($e->getMessage());
            die;
        }
        return $name;

index.phpの修正
pimpleにデータベースへの接続処理を追加します。

$ vi web/index.php
~省略~
$container = new Pimple();
$container['app'] = $app;

$container['db_master'] = $container->share(function($c){
       return \Taka512\Db\Mysql::connect(
            'localhost'/*host*/,
            'hoge'/*database*/,
            'hoge'/*id*/,
            'hogepass'/*pass*/);
        });

こんな感じでデータベースへの接続処理を追加できます。

slimでpimpleを使ってroutingを行う

pimpleを使ってslimのroutingを構造化してみます。

routes.phpの作成

configディレクトリを作成し、routing情報を記述するroutes.phpを作成します。

$ mkdir config
$ vi config/routes.php
<?php

$app->get('/', function() use ($container) {
    $container['ContentsController']->top();
  });

ContentsControllerクラスの作成

index.phpのcontrollerの中に記述していた処理をContentsControllerクラスとして独立させます。
topメソッドの中にcontroller処理を記述してます。

$ mkdir src/Taka512/Controllers
$ vi src/Taka512/Controllers/ContentsController.php
<?php

namespace Taka512\Controllers;

class ContentsController
{
    protected $app;
    protected $service;

    public function __construct(\Pimple $di) {
        $this->app = $di['app'];
        $this->init($di);
    }

    public function init(\Pimple $di) {
        $this->service = $di['NameService'];
    }

    public function top() {
        $name = $this->service->getName();
        $this->app->render('index.html.twig', array('name' => $name));
    }
}

index.phpを修正

controller処理を削除してroutes.phpの読み込み処理を追加します。

$ vi web/index.php
  9 define('PROJECT_DIR', dirname(__FILE__) . '/..');
 10 $app = new Slim(array(
 ~略~
 20 $container = new Pimple();
 21 $container['app'] = $app;
 22 $container['NameService'] = $container->share(function ($container) {
 23     return new \Taka512\Services\NameService($container);
 24 });
 25
 26 $container['ContentsController'] = $container->share(function ($container) {
 27     return new \Taka512\Controllers\ContentsController($container);
 28 });
 29
 30 require PROJECT_DIR.'/config/routes.php';
 31
 32 $app->run();

これでroutes.phpにrouting情報を定義してControllerクラスをモリモリ書いていけるようになりました。
今回の作業をした後のディレクトリ構造は下記となります。

|-- composer.json
|-- composer.lock
|-- logs
|    |-- 2013-06-29.log
|-- templates
|    |-- index.html.twig
|-- config
|    |-- routes.php
|-- vendor
|-- web
|    |-- index.php
|-- src
|    |-- Taka512
|         |-- Controllers
|              |-- ContentsController.php
|         |-- Services
|              |-- NameService.php

参考

http://nesbot.com/2012/11/5/lazy-loading-slim-controllers-using-pimple

slimでネームスペースを利用するように修正

プロジェクトディレクトリの構造化を行っていきます。
今回は前回index.phpに作成したNameServiceクラスをファイルから分離して、ネームスペースを利用するように修正します。

composer.jsonの修正

autoloadの項目を追加します。srcが設定ディレクトリで「Taka512」ネームスペースとなります。

$ vi composer.json
    "require": {
        "slim/slim": "2.*",
        "slim/extras": "2.0.*",
        "twig/twig": "1.*",
        "pimple/pimple": "1.0.0"
    },
    "autoload": {
        "psr-0": { "Taka512": "src/"}
    }
$ php composer.phar update

ディレクトリの作成

先ほどautoloadで設定したディレクトリを作成します。

$ mkdir -p src/Taka512/Services

NameServiceライブラリの作成

NameServiceクラスをファイルに分離します。今回の例ではネームスペースは「Taka512\Services」となります。
constructのPimpleの前に「\」マークをつけるのを忘れないようにしましょう。つけないと「Taka512\Service\Pimple」を探しに行ってエラーとなります。

$ vi src/Taka512/Services/NameService.php
<?php

namespace Taka512\Services;

class NameService
{
    protected $di;
    public function __construct(\Pimple $di)
    {
        $this->di = $di;
    }

    public function getName()
    {
        return 'taka512';
    }
}

index.phpの修正

index.phpからNameServiceクラスを削除して、サービスの定義の部分のネームスペースを変更します。

$container['NameService'] = $container->share(function ($container) {
    return new \Taka512\Services\NameService($container);
});

これでネームスペースを利用してライブラリをもりもり作っていけるようになりました。

slimでpimpleを使う

pimpleはphpDIコンテナを実現するためのライブラリです。
今回はslimでpimpleを使ってみます。

インストール

composer.jsonにpimpleを追加

$ vi composer.json
"require": {
    "slim/slim": "2.*",
    "slim/extras": "2.0.*",
    "twig/twig": "1.*",
    "pimple/pimple": "1.0.0"
$ php composer.phar update

index.phpを編集

32~45行目でNameServiceを定義
19~23行目でNameServiceをpimpleに登録
26行目でNameServiceをpimpleから利用してます。

$ vi web/index.php
 10 $app = new Slim(array(
 ~略~
 19 $container = new Pimple();
 20 $container['app'] = $app;
 21 $container['NameService'] = $container->share(function ($container) {
 22     return new NameService($container);
 23 });
 25 $app->get('/', function() use ($container) {
 26     $name = $container['NameService']->getName();
 27     $container['app']->render('index.html.twig', array('name' => $name));
 28   });
 29
 30 $app->run();
 32
 33 class NameService
 34 {
 35     protected $di;
 36     public function __construct(Pimple $di)
 37     {
 38         $this->di = $di;
 39     }
 41     public function getName()
 42     {
 43         return 'taka512';
 44     }
 45 }


pimpleの何が嬉しいかというとLazyロードなのでNameServiceは26行目で実際に使われるタイミングで生成されます。

slimでログを出力

前回インストールしたslim/extrasを利用してログ出力を行う設定のメモ

ログディレクトリを作成

$ mkdir -p logs
$ chmod 777 logs

index.phpを編集

$app = new Slim(array(
            'view' => new Twig,
            'templates.path' => '../templates',
            'log.writer' => new \Slim\Extras\Log\DateTimeFileWriter(array(
                    'path' => '../logs',
                    'name_format' => 'Y-m-d',
                    'message_format' => '%label% - %date% - %message%'
                    ))
                ));

$app->get('/', function() use ($app) {
    $app->getLog()->debug('log'); // - level 4
    $app->getLog()->info('log');  // - level 3
    $app->getLog()->warn('log');  // - level 2
    $app->getLog()->error('log'); // - level 1
    $app->getLog()->fatal('log'); // - level 0

    $app->render('index.html.twig', array('name' => 'taka512'));
  });

index.phpにアクセスした後にログを見てみると出力されていることが確認できます。

$ tail -f logs/2013-06-29.log
DEBUG - 2013-06-29T16:56:47+09:00 - log
INFO - 2013-06-29T16:56:47+09:00 - log
WARN - 2013-06-29T16:56:47+09:00 - log
ERROR - 2013-06-29T16:56:47+09:00 - log
FATAL - 2013-06-29T16:56:47+09:00 - log

今回の作業をした後のディレクトリ構造はこんな感じとなります。

|-- composer.json
|-- composer.lock
|-- logs
|    |-- 2013-06-29.log
|-- templates
|    |-- index.html.twig
|-- vendor
|-- web
     |-- index.php

参考
https://github.com/codeguy/Slim-Extras#log-writers

slimでtwig連携

最近slimを使う機会がありそうなのでphpのマイクロフレームワークであるslimを基礎から触った。
まずはインストールしてslimとtwigで連携するところまで行う。

composerからのslimのインストール

$ curl -s http://getcomposer.org/installer | php
$ vi composer.json
{
    "require": {
        "slim/slim": "2.*",
        "slim/extras": "2.0.*",
        "twig/twig": "1.*"
    }
}
$ php composer.phar install

index.phpの作成

$ mkdir web
$ vi web/index.php
<?php
require '../vendor/autoload.php';

use Slim\Slim;
use Slim\Extras\Views\Twig as Twig;

$app = new Slim(array(
            'view' => new Twig,
            'templates.path' => '../templates'
            ));

$app->get('/', function() use ($app) {
    $app->render('index.html.twig', array('name' => 'taka512'));
  });
$app->run();

templateを作成

$ mkdir templates
$ vi templates/index.html.twig
hello {{ name }}

これでindex.phpにアクセスするとテンプレートの内容が表示されます。
そして今回のディレクトリ構造はこんな感じとなります。

|-- composer.json
|-- composer.lock
|-- templates
|    |-- index.html.twig
|-- vendor
|-- web
     |-- index.php