わたくし、Ethnaで組まれたレガシーなWEBアプリのプロジェクトにアサインされています。
未だに新機能が追加される現役バリバリのプロジェクトですが
さすがに年季の入ったコードも多いので、リファクタも進めています。
今後のメンテナンスを見越して、リファクタついでに
これまで全く書かれていなかったユニットテストも書きはじめました。が、
Ethnaのユニットテストについてほとんど情報が出てこない状態だったので
今更ながらまとめてみました。
開発環境
PHP 5.2
Ethna 2.3.7
読むと役立ちそうな人
Ethnaで組まれたアプリでこれからユニットテストを書こうとしている奇特な人。
Simpletest?
EthnaにはSimpletestというテスティングフレームワークが組み込まれていて、
それを利用するのが一番楽そうなのですが、以下の理由で見送りました。
- テストファイルがプロジェクト内に散らばる
- プロジェクトがEthnaのお作法をちょっと外した使い方をしているのでそのままじゃ動かない
- PHPUnitの方が情報が多い
PHPUnitのインストール
ということで、PHPUnitで進めることになりました。
レガシーな環境にPHPUnitのインストール手順を記事にされている方がいらっしゃいましたので、
ありがたく参考にさせていただきPHPUnitをインストールします。
今回は、PHPUnit3.6をインストールしました。
PHPUnit3.6のドキュメント
https://phpunit.de/manual/3.6/ja/automating-tests.html
準備
とりあえず、下記3ファイルをプロジェクトディレクトリ直下に作成します。
(クラス名やファイル名はプロジェクトに合わせて適当に読み替えてください)
- PHPUnit設定ファイル
phpunit.xml
- Ethnaを起動するスクリプト
_bootstrap.php
- テスト用のヘルパーメソッドを追加した基底クラス
SAMPLE_UnitTestCase.php
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="/app_dir/_bootstrap.php"
colors="true"
convertErrorToExceptions="false"
convertNoticesToExceptions="false"
convertWarningToExceptions="false">
<testsuites>
<testsuite name="All Test Suite">
<directory suffix="Test.php">/app_dir/tests/</directory>
</testsuite>
</testsuites>
</phpunit>
<?php
error_reporting(E_ALL);
putenv('DEVELOP=true');
// Ethnaのロード
require_once '/app_dir/app/APPID_Controller.php';
require '/app_dir/APPID_UnitTestCase.php';
// Ethnaのインスタンス化
new APPID_Controller();
<?php
/**
* このファイルを継承してテストクラスを書きます
*/
class APPID_UnitTestCase extends PHPUnit_Framework_TestCase
{
protected $ctl;
protected $classFactory;
protected $backend;
protected $session;
public function setUp()
{
// オブジェクトの設定
$this->ctl = Ethna_Controller::getInstance();
$this->classFactory = $this->ctl->getClassFactory();
$this->backend = $this->ctl->getBackend();
$this->session = $this->backend->getSession();
}
public function createActionClass($actionName)
{
$actionClassName = $this->ctl->getActionClassName($actionName);
return new $actionClassName($this->backend);
}
public function createActionForm($actionName)
{
$formName = $this->ctl->getActionFormName($actionName);
return new $formName($this->ctl);
}
public function createViewClass($forwardName)
{
$viewClassName = $this->ctl->getViewClassName($forwardName);
return new $view_class_name($this->backend, $forwardName, $this->ctl->_getForwardPath($forwardName));
}
}
準備ができたのでテストを書いてきます。
ActionClassをテストする
アクションクラスは大体こんな形になっていると思いますが、
<?php
class APPID_Form_SomeActionClass extends Ethna_ActionForm {
var $form = array();
}
class APPID_Action_SomeActionClass extends Ethna_ActionClass {
function prepare() {
// 何かしらの処理
return null;
}
function perform() {
// 何かしらの処理
return 'someResult';
}
}
こんな感じでテストをかきます。
<?php
class SomeActionClassTest extends APPID_UnitTestCase
{
private $ac;
private $actionName;
/**
* テストメソッド開始前に毎回呼ばれる
*/
public function setUp()
{
// 基底クラスのsetUpをコールして、テストの実行に必要なインスタンスを準備
parent::setUp();
$this->actionName = 'SomeActionClass';
$this->ac = $this->createActionClass($this->actionName);
$this->ac->af = $this->createActionForm($this->actionName);
}
public function testPrepareNormal()
{
// nullが返ること
$this->assertSame(null, $this->ac->prepare());
}
public function testPerformNormal()
{
// someResultが返ること
$this->assertSame('someResult', $this->ac->perform());
}
}
ViewClassをテストする
ビュークラスも同様にテストがかけます。
<?php
class APPID_View_someViewClass extends Ethna_ViewClass
{
function preforward()
{
// 何かしらの処理
}
}
class someViewClassTest extends APPID_UnitTestCase
{
private $vc;
private $actionName;
private $forwardName;
public function setUp()
{
parent::setUp();
$this->forwardName = 'someViewClass';
$this->vc = $this->createViewClass($this->forwardName);
}
public function testPreforwardNormal()
{
$this->assertSome(null, $this->vc->preforward());
}
}
ここまではなんの問題もないですね。
モック・スタブ
テストを進めていくとマネージャーやオブジェクト、クラスの一部のメソッドをモック化したい場面が出てくると思います。
この場合は、PHPUnitで作成したモッククラスを注入します。
Ethna2.3.7+PHP5.2の組み合わせはメンバ変数、メソッドの全てがパブリックなので
注入は特に難しく考えずに上書きしてやることで対応可能です。
マネージャークラスのモック化
以下のようにbackend
のgetManager
メソッドからマネージャークラスを返すコードがあったとします。
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
$manager = $this->backend->getManager('someManagerClass');
return $manager->someMethod('abc');
}
}
モックにしたいのはsomeManagerClass
のsomeMethod
ですが
マネージャークラスはbackend
がインスタンスを返しているので
someManagerClass
をモック化しただけでは$manager
に渡すことができません。
この場合は、backend
のgetManager
メソッドをモック化して、
モック化したsomeManagerClass
を返すようにします。
backend
は、テスト対象クラスのbackend
メンバ変数をモックのbackend
で上書きしてやります。
PHPUnitで作るモック・スタブの詳細は公式ドキュメントを御覧ください。
// マネージャークラスは予めrequireしてやる必要があります
require_once "app_dir/app/someManagerClassManager.php";
public function testManager()
{
// マネージャークラスのモックを作成
$manager = $this->getMockBuilder($this->ctl->getManagerClassName('someManagerClass'))
->disableOriginalConstructor()
->getMock();
$manager->expects($this->once())
->method('someMethod')
->with('abc')
->will($this->returnValue(true));
// backendのモックを作成
$backend = $this->getMockBuilder(get_class($this->backend))
->disableOriginalConstructor()
->setMethods(array('getManager'))
->getMock();
// $this->backend->getManager('someManagerClass')でモックを返すようにする
$backend->expects($this->any())
->method('getManager')
->with('someManagerClass')
->will($this->returnValue($manager));
// テスト対象のアクションクラスのbackendをモックで上書き
$this->ac->backend = $backend;
// trueが返ること
$this->assertSome(true, $this->ac->prepare());
}
sessionクラスのモック化
同様にsessionクラスもモック化したsessionを上書きしてやると
セッションを利用したコードもテストできます。
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
if ($this->session->get('account_id') === null) {
return false;
}
return true;
}
}
public function testPrepare()
{
// sessionクラスをモック化
$session = $this->getMockBuilder(get_class($this->backend->session))
->disableOriginalConstructor()
->getMock();
$session->expects($this->once())
->method('get')
->with('account_id')
->will($this->returnValue('hoge'));
// テスト対象のアクションクラスのsessionをモックで上書き
$this->ac->session = $session;
// trueが返ること
$this->assertSome(true, $this->ac->prepare());
}
クラスの一部メソッドをモック化
この一部モック化ですが、
下のPHP標準関数や、メソッド内でnewされているクラスをモック化する場合によく使います。
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
$id = 'aaa';
return $this->someMethod($id);
}
function someMethod($id) {
if ($id !== null) {
return true;
}
return false;
}
}
public function testPrepare()
{
$this->actionName = 'SomeActionClass';
// actionクラスを一部モック化
$action = $this->getMockBuilder($this->ctl->getActionClassName($this->actionName))
->disableOriginalConstructor()
->setMethods(array('someMethod'))
->getMock();
$action->expects($this->once())
->method('someMethod')
->will($this->returnValue(true));
// trueが返ること
$this->assertSome(true, $action->prepare());
}
PHPの標準関数のモック化
PHPは便利関数が多いので、よく使われる標準関数ですが
このバージョンのPHPだとそのままではモックにできません。
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
$arr = array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5);
// この標準関数json_encodeはこのままではモックにできない
return json_encode($arr);
}
}
この場合は、以下のようにクラスのメソッドで標準関数をラッピングして
そのメソッドをモックにすることで対応します。
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
$arr = array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5);
// このようにクラスのメソッドでラッピングして、テスト時には$this->json_encodeをモックにする
return $this->json_encode($arr);
}
function json_encode($value, $options = 0, $depth = 512)
{
return json_encode($value, $option, $depth);
}
}
メソッド内でnewしているクラス
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
// このHogeClassはこのままではモックにできない
$hoge = new HogeCLass();
return $hoge->otherMethod();
}
}
こちらも以下のように、クラスメソッドでラッピングしてやります。
class APPID_Action_someActionClass extends Ethna_ActionClass
{
function prepare()
{
// テストのときはcreateHogeClassをモックにする
$hoge = $this->createHogeClass();
return $hoge->otherMethod();
}
function createHogeClass()
{
return new HogeClass();
}
}
これらのパターンを組み合わせれば、
Ethnaのほとんどのケースでユニットテストがかけると思います。
まとめ
これでユニットテストを書く環境が整いました。
あとは、テストを習慣づけることですがこれが一番難しいですね。