PowerCMS Xのプラグインを書いた際にPHPUnitでテストを行うことができたら良いのでは?と最近考えていました。機能が設計通り動作しているかを確認できますし、PowerCMS X本体がバージョンアップした時の動作確認も容易になると考えます。そこで、テストを書くことは可能なのか調べてみました。題材は引き続きAlgolia + PowerCMSシリーズです。
事前準備
プラグインのディレクトリでcomposer require --dev phpunit/phpunit
を実行します。題材となるAlgoliaSupportプラグインでは、既にAlgolia Search API Clientをcomposerでインストールしていましたが、まだcomposerを使用していないプロジェクトでは事前にcomposer init
を実行してください。PHP Archive (PHAR)を使う方法等もあるようですので詳細は「1. PHPUnit のインストール — PHPUnit latest Manual」を参照してください。
テストの準備・PowerCMS Xのインスタンス生成
簡単そうなことからテストを書いて、テストが実現できるかを探りました。PHPUnitを使用したテストの書き方は「2. PHPUnit 用のテストの書き方 — PHPUnit latest Manual」に解説がありますので、それに倣って記述します。
ここでまず気になることは、どのようにすればPowerCMS Xをコマンドラインで動かすことができるのか?、ではないでしょうか。これはtools
の中のスクリプトが参考になると思いますし、「PHPによるプログラミング・ガイド (クラス Prototype編) | PowerCMS X」にも解説があります。テストの前にPowerCMS Xのインスタンスを生成するためにsetUp()
を記述します。setup()
の解説は「4. フィクスチャ — PHPUnit latest Manual」にあります。
余談になりますが、AlgoliaSupportではまだ.env
に設定を書く仕様が残っているので、.env
も使用できるようにします。
protected function setUp(): void {
$app = new Prototype();
$app->init_tags = true;
$app->plugin_paths = [ '/var/www/powercmsx/customized_files/plugins' ];
$app->use_plugin = true;
$app->init();
$this->app = $app;
$dotenv = Dotenv\Dotenv::createImmutable( __DIR__ . '/../' );
$dotenv->load();
}
init_tags
をtrue
にするとダイナミック・タグの初期化が行われます。またuse_plugin
をtrue
にするとプラグインの初期化が行われます。私は自作プラグインを独自のディレクトリに入れているので、plugin_paths
でその場所を示します。
ここまでの内容でPowerCMS Xの実態であるPrototypeインスタンスが生成できました。なお、new Prototype()
の時に何も指定していないので、$app->id
はPrototype
になります。このid
はWorkerを実行したときはWorker
、pt-view.php
経由で動的にページを表示した場合はBootstrapper
にする必要があるので注意しましょう。
モデルのオブジェクトを取得して値を確認する
Prototypeのインスタンスが準備できたならモデルのオブジェクトを取得するのは容易なはずなので、AlgoliaSupportプラグインには全く関係無いのですがschool(学校)モデルのオブジェクトの取得を試行し、意図したオブジェクトが取得できたかを確認します。以下のコードはid
が1
のオブジェクトの学校名が盈進中学高等学校
であることを確認します。
public function test_オブジェクトの取得() {
$db = $this->app->db;
$object = $db->model( 'school' )->load( 1 );
$this->assertSame( '盈進中学高等学校', $object->name );
}
php ./vendor/bin/phpunit test/AlgoliaSupportTest.php
を実行してテスト結果を確認します。このテストは無事成功しました。値を確認するアサーションメソッドの一覧は「1. アサーション — PHPUnit latest Manual」にあります。
テンプレートタグの出力を確認する
テンプレートタグのテストが書けるのか?はみなさんがきっと気になることだと思います。私もとても興味がありました。
以下のコードはMTAlgoliaSupportAppID
という独自のファンクションタグが正しい値を出力するかをテストします。テンプレート文字列をビルドするには、「PHPによるプログラミング・ガイド (クラス Prototype編) | PowerCMS X」にあるように$app->build( $tmpl );
とします。$tmpl
はテンプレートの内容です。簡単ですね。
public function test_AppIDの取得() {
$app = $this->app;
$tmpl = '<mt:algoliasupportappid />';
$out = $app->build( $tmpl );
$this->assertSame( $_ENV['APP_ID'], $out );
}
もちろん<mt:schools id="10" workspace_id="0"><mt:schoolname /></mt:schools>
のようなブロックタグとファンクションタグを組み合わせたテンプレート文字列でも大丈夫です。テンプレートタグに関するテストも無事成功しました。
post_saveコールバックが正しく動作するかを確認する
さらにハードルが上がります。AlgoliaSupportプラグインではschool(学校)モデルを保存した時(post_save
)、AlgoliaのAPIと通信をして検索データを保存し、返値でobjectIDを受け取ります。そして、そのobjectIDをモデルに保存する処理を記述しています。プラグインで実装したpost_save
コールバックが正しく実行されるかを確かめます。
コツはオブジェクトを新規作成・値をセットした後、保存した時に$app->run_callbacks()
を実行することでした。管理画面を通して操作していると何もしなくてもpost_save
コールバックが実行されますが、コマンドからだと少し違うようです。$object
はPADOMySQLのインスタンスであることが関係していると想像しています。
public function test_記事保存時にAlgoliaのobjectIDの取得して保存する() {
$app = $this->app;
$db = $app->db;
$model = 'school';
$app->get_scheme_from_db( $model );
$app->init_callbacks( $model, 'post_save' );
$object = $db->model( $model )->new([
'name' => 'テスト高校',
'status' => $app->status_published( $model ),
'rev_type' => 0,
]);
$object->save();
$callback = [ 'name' => 'post_save' ];
$app->run_callbacks( $callback, $model, $object );
$this->assertIsNumeric( $object->algolia_object_id );
}
上記の内容でAlgoliaのobjectIDはモデルに保存されており、post_saveコールバックに関するテストも無事成功しました。テストに使用したオブジェクトを消すコードを入れても良いかもしれません。
CSVのインポート
まだこれはテスト可能か調査中です。コマンドラインからPOSTして実行できるのか、とても興味があります。また改めて検証・レポートします。
※1/28追記: テスト対象はpost_import
が呼ばれた時の動作なので、CSVをアップロードする必要はなくいくつか適当にオブジェクトを作りながらpost_import
を呼べば良いだけかもしれません。そうであれば、post_saveコールバックと同じく$app->run_callbacks()
を実行するだけですね。
- CSVアップロードしてオブジェクトが登録されるかどうかはPowerCMS X本体側のテスト対象と考えます
- オブジェクト1つ保存する度に呼ぶ件は「PowerCMS XでCSVをインポートした時の独自処理をプラグインで実装する」をご覧ください
まとめ
PowerCMS XのプラグインもPHPUnitでテストが可能なことが分かりました。ここまでご紹介した内容で全ての機能がテスト可能かと言えば、まだ難しいかもしれません。しかし、テストを書きやすい部分からテストを書いて品質の維持向上に役立てていきたいと考えています。
テストの全貌
最後にテストファイルの全貌を掲載します。プライベートメソッドのテストについては「privateとprotectedメソッドをPHPUnitでテストする方法 - Qiita」を参考にさせて頂きました。情報ありがとうございます。
<?php
use PHPUnit\Framework\TestCase;
require_once 'vendor/autoload.php';
require_once '/var/www/powercmsx/class.Prototype.php' ;
class AlgoliaSupportTest extends TestCase {
protected $app;
protected function setUp(): void {
$app = new Prototype();
$app->init_tags = true;
$app->plugin_paths = [ '/var/www/powercmsx/customized_files/plugins' ];
$app->use_plugin = true;
$app->init();
$this->app = $app;
$dotenv = Dotenv\Dotenv::createImmutable( __DIR__ . '/../' );
$dotenv->load();
}
/**
* privateメソッドを実行する.
* @param string $method_name privateメソッドの名前
* @param array $params privateメソッドに渡す引数
* @return mixed 実行結果
* @throws \ReflectionException 引数のクラスがない場合に発生.
*
* 参考: https://qiita.com/ponsuke0531/items/6dc6fc34fff1e9b37901
*/
private function do_method( string $method_name, array $params = [] ) {
$algolia_support = new AlgoliaSupport();
$reflection = new \ReflectionClass( $algolia_support );
$method = $reflection->getMethod( $method_name );
$method->setAccessible( true );
return $method->invokeArgs( $algolia_support, $params );
}
public function test_オブジェクトの取得() {
$db = $this->app->db;
$object = $db->model( 'school' )->load( 1 );
$this->assertSame( '盈進中学高等学校', $object->name );
}
public function test_インデックスへの接続() {
$db = $this->app->db;
$object = $db->model( 'school' )->load( 1 );
$algolia_object_id = $object->algolia_object_id;
$index = $this->do_method( 'init_algolia_client', [ $_ENV['INDEX_NAME'] ] );
$result = $index->getObject( $algolia_object_id );
$this->assertSame( '福山市千田町千田487-4', $result['address'] );
}
public function test_AppIDの取得() {
$app = $this->app;
$tmpl = '<mt:algoliasupportappid />';
$out = $app->build( $tmpl );
$this->assertSame( $_ENV['APP_ID'], $out );
}
public function test_記事保存時にAlgoliのaobjectIDの取得して保存する() {
$app = $this->app;
$db = $app->db;
$model = 'school';
$app->get_scheme_from_db( $model );
$app->init_callbacks( $model, 'post_save' );
$object = $db->model( $model )->new([
'name' => 'テスト高校',
'status' => $app->status_published( $model ),
'rev_type' => 0,
]);
$object->save();
$callback = [ 'name' => 'post_save' ];
$app->run_callbacks( $callback, $model, $object );
$this->assertIsNumeric( $object->algolia_object_id );
}
}