codeception+phalcon/incubatorでphalconの単体テストをやってみました。
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と定義しているファイル)が各テストの前に実行されます。
読み込まれるファイルとテスト実行の流れは以下のとおりです。
- ルートの(testsフォルダ)のbootstrapファイルの読み込み(bootstrapファイルの名前はcodeception.ymlで定義)
- 各スイート(ディレクトリ)のテストクラスの読み込み+インスタンス化(メソッド単位でインスタンス化されるっぽい)
- 各スイートのbootstrapファイルの読み込み
- 各テストコード実行(setup→コード実行→teardown)
- 2-4の繰り返し
<?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日目の記事です。