2015-12-07

codeceptionでphalconの単体テストしてみた【incubator編】

codeceptionphalcon/incubatorphalconの単体テストをやってみました。

codeceptionではブラウザと実際に稼働しているアプリケーションを使ったAcceptanceTest、アプリケーション(各フレームワーク対応)をテスト実行時に内部で動かすFunctionalTest、従来型の単体テストを行うUnitTestがあります。今回はUnitTestでの単体テストの方法を書いていきます。

1. codeceptionのインストール

以下のコマンドを実行してインストールします。

$ sudo curl -LsS http://codeception.com/codecept.phar \
> -o /usr/local/bin/codecept
$ sudo chmod a+x /usr/local/bin/codecept

2. phalcon/incubatorのインストール

composerでphalcon/incubatorをインストールします。composer.jsonはこんな感じで↓

{
    "require": {
        "phalcon/incubator": "dev-master"
    }
}

あとは以下のコマンドを叩くだけ。

$ cd {phalconのアプリケーションルートディレクトリ}
$ composer install

3. codeceptionの設定

まずはbootstrapでテンプレートを作ります。

$ codecept bootstrap -e

codeceptionのテストですが、codecept runコマンドを実行したディレクトリのcodeception.ymlを読み取って、記載された設定に従ってテストを行うことになります。

codeception.ymlはこんな感じです。

actor: Tester
paths:
    tests: tests
    log: tests/_output
    data: tests/_data
    support: tests/_support
    envs: tests/_envs
settings:
    bootstrap: _bootstrap.php
    colors: true
    memory_limit: 1024M
extensions:
    enabled:
        - Codeception\Extension\RunFailed
modules:
    config:
        Db:
            dsn: ''
            user: ''
            password: ''
            dump: tests/_data/dump.sql

paths.testsはテストコードが格納されているディレクトリで、ここに格納されているテストコードを対象にテストを実行することになります。

-eオプションを付けてテンプレートを作成すると、suite(テストのグループ)は何も作成されないので、以下のコマンドで単体テスト用のsuiteを作成します。codeceptionでテストを実行するためには必ずsuiteを作成する必要があります。

$ codecept generate:suite unit
$ codecept build

そうするとtestsディレクトリの中にunitディレクトリとunit.suite.ymlという設定ファイルが作成されます。全体的なディレクトリ構造はこんな感じになります。

.
|-- app
|-- codeception.yml
|-- composer.json
|-- composer.lock
|-- index.html
|-- public
|-- tests
|   |-- _bootstrap.php
|   |-- _data
|   |   `-- dump.sql
|   |-- _envs
|   |-- _output
|   |-- _support
|   |   |-- Actions.php
|   |   |-- _generated
|   |   |   `-- UnitTesterActions.php
|   |   |-- Helper
|   |   |   `-- Unit.php
|   |   `-- UnitTester.php
|   |-- unit
|   |   |-- _bootstrap.php
|   |   `-- UTSampleTest.php
|   `-- unit.suite.yml
`-- vendor

codeceptionでは指定ディレクトリ内(今回はtests)の***.suite.ymlの設定ファイルを読み込んだ後、対象のディレクトリ内のテストが順次実行されることになります。また、boostrapファイル(codeception.ymlで_bootstrap.phpと定義しているファイル)が各テストの前に実行されます。

読み込まれるファイルとテスト実行の流れは以下のとおりです。

  1. ルートの(testsフォルダ)のbootstrapファイルの読み込み(bootstrapファイルの名前はcodeception.ymlで定義)
  2. 各スイート(ディレクトリ)のテストクラスの読み込み+インスタンス化(メソッド単位でインスタンス化されるっぽい)
  3. 各スイートのbootstrapファイルの読み込み
  4. 各テストコード実行(setup→コード実行→teardown)
  5. 2-4の繰り返し
ルートのbootstrapで各テストで必要な定数の定義やクラスのローディングをすることになります。 具体的には以下の様な感じになります。

<?php

if (!defined('APP_PATH')) {
    define('APP_PATH', __DIR__ . '/..');
}
Codeception\Util\Autoload::addNamespace('', APP_PATH . '/tests/unit');
require APP_PATH . '/vendor/autoload.php';

Codeception\Util\Autoload::addNamespaceでは名前空間とディレクトリパスを指定することで対象ファイルのクラスを自動的に読み込んでくれます。 _supportフォルダ内に入れたものも自動的に読み込まれる仕様らしいので、そこに入れてもOK。

あとは以下のコマンドをアプリケーションのルートディレクトリで実行すればOK。

$ codecept run

–debugオプションを付けるとcodecept_debug()関数での出力も表示されるのでローカルでテストするときは付けたほうが良いかも。

4. テストコードの作成

tests/unit/UTSampleTest.phpに以下のコードを書きます。

<?php

use Phalcon\Test\FunctionalTestCase as PhalconTestCase;

class UTSampleTest extends PhalconTestCase
{
    public function setUp()
    {
        $config = include APP_PATH . '/app/config/config.php';
        include APP_PATH . '/app/config/loader.php';
        include APP_PATH . '/app/config/services.php';
        
        $this->di = $di;
        $this->application = new \Phalcon\Mvc\Application($this->di);
        $this->config = $config;
    }
}

Phalcon\Test\FunctionalTestCaseはphalcon/incubatorのクラスになりますが、setUp内でparent::setup()をすると、FunctionalTestCaseの継承元のModelTestCaseで色々と不都合が起きてしまうため(※1)、parent::setUp()していません。その代わりに、parent::setUp()で行っている、DIの設定、アプリケーションの設定を明示的に行っています。

FunctionalTestCaseではプロパティの$applicationを使って内部でphalconを動かします。具体的には以下の様なコードになります。

//write your request
...

// start test
$this->dispatch('/foo/bar');

// assertion
//codecept_debug($this->getContent());
$this->assertController('foo');
$this->assertAction('bar');
$this->assertResponseCode(200);
$res_xml = new \SimpleXMLElement($this->getContent());
$this->assertEquals((string)$res_xml->hoge[0], "fuga");

(※1 $this->setDb();でデフォルト引数でmysqlを利用していたりするので、postgresql使ってたりするとエラーになる)

5. リクエストの書き方

コントローラ上で$this->request->getQuery()とか$this->request->getRawBody()とか、request変数経由で値を取得している場合は、request自体がDIコンテナを利用しているので、以下のようにしてテストすることが出来ます。

$_SERVER["REQUEST_METHOD"] = $method;
$stub = $this->getMockBuilder("\\Phalcon\\Http\\Request")
             ->setMethods(array("getRawBody", "getBasicAuth"))
             ->getMock();
$stub->method('getRawBody')
     ->willReturn($body);

foreach ($headers as $key => $value) {
    $_SERVER["HTTP_" . strtoupper($key)] = $value;
}

$stub->method('getBasicAuth')
     ->willReturn($user);

$this->di->set("request", $stub, true);

\Phalcon\Http\Requestのメソッド判定やURLパラメータ、ヘッダの取得は$_SERVER、$_GETを利用しているので、それらのスーパーグローバル変数を書き換えればOKです。getRawBodyに関してはphp://inputを出力しているので、php://inputに入力すれば良い気がしますが、何となく扱いづらそうな感じがしたので、今回はスタブでオーバーライドしてます。

6. その他

phalconはDIが色々便利なので、テストに使いたいコンポーネントはDI使うと良い気がします。例えばSendGridなんかを使ってメール送信をする場合、テストの都度メールが飛ぶのは面倒なので以下のようなスタブをDIコンテナに突っ込んでテストするとメールが飛ばなくなりますし、ちゃんとコールされたかどうかの検証も出来ます。

$di->setShared('sendGrid', function() use($config) {
    $stub = $this->getMockBuilder('\SendGrid')
                         ->setConstructorArgs([$config->username, $config->password, []])
                         ->setMethods(['send'])
                         ->getMock();
    
    $stub->method('send')
         ->willReturn('hogehoge');
    
    return $stub;
});

以下、その他ハマりポイント&TIPS。

codecept_debugでPhalconのモデルをデバッグしようとするとメモり使い果たしエラー発生(Allowed memory size of *** bytes exhausted) →モデルの中が良い感じに循環参照になっているため、json_encodeするなりしてプロパティだけ取得して出力する必要があります。

codecept_debug(json_encode($object));

Class not foundって言われるとき →だいたいrequireできてないので、読み込まれるファイルの順番とオートローダの記載を要確認。

指定のディレクトリのテストのみ実行させたい

$ codecept run tests/unit/hoge

指定のクラスのテストのみ実行させたい

$ codecept run tests/unit/hoge/HogeControllerTest.php

指定のメソッドだけ実行させたい

$ codecept run tests/unit/hoge/HogeControllerTest.php:testFugaAction

 

この投稿はPhalcon Advent Calendar 2015の 7日目の記事です。

このエントリーをはてなブックマークに追加