テストフックのコールバック の必然的な結果としてこれが見られるかもしれません。
問題:私はプラグインの外で定義されたMy_Notice
クラスの新しいインスタンスを作成するクラスをテストしたいです(それを「メインプラグイン」と呼びましょう)。
私のユニットテストはMy_Notice
については何も知りません。なぜならそれはサードパーティのライブラリ(正確には別のプラグイン)で定義されているからです。したがって、私は(私の知る限りでは)これらの選択肢があります。
My_Notice
クラスをスタブする:保守が難しい@gmazzapからの答え は、この種の依存関係を作成することは避けるべきであると指摘しています。そして、スタブクラスを作成するという考えは私には良くないようです。私はむしろ "Main Plugin"のコードを含めたいと思います。
しかし、そうでなければどうすればよいかわかりません。
これが私がテストしようとしているコードの例です。
class My_Admin_Notices_Handler {
public function __construct( My_Notices $admin_notices ) {
$this->admin_notices = $admin_notices;
}
/**
* This will be hooked to the `activated_plugin` action
*
* @param string $plugin
* @param bool $network_wide
*/
public function activated_plugin( $plugin, $network_wide ) {
$this->add_notice( 'plugin', 'activated', $plugin );
}
/**
* @param string $type
* @param string $action
* @param string $plugin
*/
private function add_notice( $type, $action, $plugin ) {
$message = '';
if ( 'activated' === $action ) {
if ( 'plugin' === $type ) {
$message = __( '%1s Some message for plugin(s)', 'my-test-domain' );
}
if ( 'theme' === $type ) {
$message = __( '%1s Some message for the theme', 'my-test-domain' );
}
}
if ( 'updated' === $action && ( 'plugin' === $type || 'theme' === $type ) ) {
$message = __( '%1s Another message for updated theme or plugin(s)', 'my-test-domain' );
}
if ( $message ) {
$notice = new My_Notice( $plugin, 'wpml-st-string-scan' );
$notice->text = $message;
$notice->actions = array(
new My_Admin_Notice_Action( __( 'Scan now', 'my-test-domain' ), '#' ),
new My_Admin_Notice_Action( __( 'Skip', 'my-test-domain' ), '#', true ),
);
$this->admin_notices->add_notice( $notice );
}
}
}
基本的に、このクラスはactivated_plugin
にフックされるメソッドを持っています。このメソッドはコンストラクタに渡されたMy_Notices
インスタンスによってどこかに格納される "notification"クラスのインスタンスを構築します。
My_Notice
クラスのコンストラクタは2つの基本的な引数(UIDと "group")を受け取り、いくつかのプロパティを設定します(同じ問題はMy_Admin_Notice_Action
クラスにもあることに注意してください)。
My_Notice
クラスをインジェクトされた依存関係にするにはどうすればよいですか?
もちろん、連想配列を使って、 "Main Plugin"によってフックされ、その配列をクラスの引数に変換する何らかのアクションを呼び出すことができますが、それは私にはきれいに見えません。
単独で完全にテスト可能にするには、コードはクラスを直接インスタンス化することを避け、オブジェクト内に必要なオブジェクトを注入する必要があります。
ただし、このルールには少なくとも2つの例外があります。
ArrayObject
最初のケースは簡単に説明できます。それは言語に埋め込まれたものであるため、機能すると仮定します。それ以外の場合は、PHPコア関数またはreturn
…などのステートメントもテストする必要があります。
2番目のケースは、値オブジェクトの性質に関連しています。実際には:
つまり、値オブジェクトは、たとえば文字列のように、それ自体で不変の型として見ることができます。
一部のコードが$myEmail = '[email protected]'
を実行する場合、その文字列をモックすることを誰も気にせず、同様に、new Email('[email protected]')
(Email
が不変であり、おそらくfinal
であると仮定)のような行をモックすることを気にするべきではありません。
あなたのコードから推測できることは、My_Admin_Notice_Action
は、値オブジェクトになる/値オブジェクトになるための良い候補です。しかし、コードを見なくてはわかりません。
同じMy_Notice
を言うことはできませんが、それは単なる推測です。
別のクラスでインスタンス化されたクラスが上記の2つのケースのいずれでもない場合は、確実に注入することをお勧めします。
ただし、OPの例のように、構築されるクラスにはコンテキストに依存する引数が必要です。
この場合「1つ」の答えはありませんが、場合に応じてすべて有効な異なるアプローチがあります。
簡単なアプローチは、「オブジェクトコード」を「クライアントコード」から分離することです。クライアントコードは使用するオブジェクトのコードです。
このようにして、単体テストを使用してオブジェクトコードをテストし、クライアントコードテストを機能テストや統合テストに任せることができます。この場合、分離を心配する必要はありません。
あなたの場合、次のようなものになります。
add_action( 'activate_plugin', function( $plugin, $network_wide ) {
$message = My_Admin_Notices_Message( 'plugin', 'activated' );
if ( $message->has_text() ) {
$notice = new My_Notice( $plugin, $message, 'wpml-st-string-scan' );
$notice->add_action( new My_Admin_Notice_Action( 'Scan now', '#' ) );
$notice->add_action( new My_Admin_Notice_Action( 'Skip', '#', true ) );
$notices = new My_Notices();
$notices->add_notice( $notice );
$handler = new My_Admin_Notices_Handler( $notices );
$handler->handle_notices();
}
}, 10, 2);
コードを推測し、存在しない可能性のあるメソッドやクラス(My_Admin_Notices_Message
など)を記述しましたが、ここでのポイントは、上記のクロージャーにはオブジェクトのインスタンス化と「使用」に必要なすべてのクライアントコードが含まれていることです。これらのオブジェクトのいずれも他のオブジェクトをインスタンス化する必要がないため、オブジェクトを分離してテストできますが、それらはすべてコンストラクターまたはメソッドパラメーターとして必要なインスタンスを受け取ります。
上記のアプローチは小さなプラグイン(またはプラグインの残りの部分から分離できる小さな部分)ではうまくいくかもしれませんが、より大きなコードベースでは、そのアプローチのみを使用すると、クロージャーで大きなクライアントコードで終わることがあります物事は、テストと保守が非常に困難です。
そのような場合、工場があなたを助けるかもしれません。ファクトリは、他のオブジェクトを作成する唯一のスコープを持つオブジェクトです。ほとんどの場合、同じタイプのオブジェクト(同じインターフェースを実装)に特定のファクトリーを用意しておくとよいでしょう。
工場では、上記のコードは次のようになります。
add_action( 'activate_plugin', function( $plugin, $network_wide ) {
$notice = $notice_factory->create_for( 'plugin', 'activated' );
if ( $notice instanceof My_Notice_Interface ) {
$handler_factory = new My_Admin_Notices_Handler_Factory();
$handler = $handler_factory->build_for_notices( [ $notice ] );
$handler->handle_notices();
}
}, 10, 2);
すべてのインスタンス化コードは工場にあります。適切な引数が与えられると期待されるクラスを生成する(または間違った引数が与えられると期待されるエラーを生成する)ことをテストする必要があるため、ファクトリを単独でテストできます。
オブジェクトをインスタンスを作成する必要がないため、他のすべてのオブジェクトを単独でテストできます。実際、インスタンス化コードはすべて工場内にあります。
もちろん、値オブジェクトはファクトリーを必要としないことを覚えておいてください...文字列のファクトリーを作成するようなものです...
さまざまな理由で、他のオブジェクトをインスタンス化するコードを変更できない場合があります。例えば。コードはサードパーティ、下位互換性などです。
これらの場合、インスタンス化されるクラスをロードせずにテストを実行できる場合は、いくつかのスタブを作成できます。
あなたがするコードがあると仮定しましょう:
class Foo {
public function run_something() {
$something = new Something();
$something->run();
}
}
Something
クラスをロードせずにテストを実行できる場合は、テスト(「スタブ」)のためだけにカスタムSomething
クラスを作成できます。
スタブは非常にシンプルに保つことを常にお勧めします、例えば:
class Something{
public function run() {
return 'I ran'.
}
}
テストを実行すると、Something
クラスのこのスタブを含むファイルをロードできます(たとえば、テストsetUp()
から)。テスト対象のクラスがnew Something
をインスタンス化するとき、それを分離してテストします。セットアップは非常に簡単であり、設計どおりに期待どおりに動作するように作成できます。
もちろん、これを維持するのはそれほど簡単ではありませんが、通常はサードパーティのコードを単体テストしないことを考慮すると、これを行う必要はほとんどありません。
ただし、これは、WordPressオブジェクトをインスタンス化するプラグイン/テーマコードを分離してテストする場合に役立ちます(例:WP_Post
)。
Mockery
(PHP単体テスト用のツールを提供するライブラリ)を使用すると、これらのスタブの記述を回避することもできます。 Mockeryでは、「インスタンスモック」(別名「オーバーロード」)を使用して、新しいインスタンスの作成をインターセプトし、モックに置き換えることができます。 この記事 それを行う方法をかなりよく説明しています。
テストするコードにハード依存関係があり(new
を使用してクラスをインスタンス化する)、インスタンス化されるクラスをロードせずにテストをロードする可能性がない場合、コードに触れることなく(または、その周りの抽象化レイヤー)。
ただし、テストbootstrapファイルは多くの場合、絶対的な最初のものとしてロードされることに注意してください。したがって、スタブのロードを強制できる少なくとも2つのケースがあります。
コードはオートローダーを使用します。この場合、オートローダーをロードする前にスタブをロードすると、new
が使用されるとクラスがすでに検出され、オートローダーがトリガーされないため、「実際の」クラスはロードされません。
コードは、クラスを定義/ロードする前にclass_exists
をチェックします。その場合、最初にスタブをロードすると、「実際の」クラスがロードされなくなります。
他のすべてが失敗した場合、ハード依存関係をテストするために別のことができます。
多くの場合、ハードな依存関係はプライベート変数として保存されます。
class Foo {
public function __construct() {
$this->something = new Something();
}
public function run_something() {
return $this->something->run();
}
}
このような場合、ハード依存関係をモック/スタブに置き換えることができますafterインスタンスが作成されます。
これは、private
プロパティでさえPHPで簡単に置き換えることができるからです。実際には、複数の方法で。
詳細を掘り下げることなく、クロージャーバインディングを使用してそれを行うことができます。スコープに使用できる "Andrew"というライブラリ を作成しました。
Andrew(およびMockery)を使用して上記のクラスFoo
をテストすると、次のことができます。
public function test_run_something() {
$foo = new Foo();
$something_mock = Mockery::mock( 'Something' );
$something_mock
->shouldReceive('run')
->once()
->withNoArgs()
->andReturn('I ran.');
// changing proxy properties will change private properties of proxied object
$proxy = new Andrew\Proxy( $foo );
$proxy->something = $something_mock;
$this->assertSame( 'I ran.', $foo->run_something() );
}