it-swarm-ja.com

単体テスト - 依存関係の処理

テストフックのコールバック の必然的な結果としてこれが見られるかもしれません。

問題:私はプラグインの外で定義されたMy_Noticeクラスの新しいインスタンスを作成するクラスをテストしたいです(それを「メインプラグイン」と呼びましょう)。

私のユニットテストはMy_Noticeについては何も知りません。なぜならそれはサードパーティのライブラリ(正確には別のプラグイン)で定義されているからです。したがって、私は(私の知る限りでは)これらの選択肢があります。

  1. My_Noticeクラスをスタブする:保守が難しい
  2. サードパーティのライブラリから必要なファイルを含める:これはうまくいくかもしれませんが、私は自分のテストをより孤立させています
  3. クラスを動的にスタブします。それが可能であるかどうかはわかりませんが、クラスのモックと非常によく似ています。ただし、同じクラスの定義も作成する必要があるためです。

@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"によってフックされ、その配列をクラスの引数に変換する何らかのアクションを呼び出すことができますが、それは私にはきれいに見えません。

7

オブジェクトの注入は本当に必要ですか?

単独で完全にテスト可能にするには、コードはクラスを直接インスタンス化することを避け、オブジェクト内に必要なオブジェクトを注入する必要があります。

ただし、このルールには少なくとも2つの例外があります。

  1. クラスは言語クラスです。 ArrayObject
  2. クラスは適切です "値オブジェクト"

コアオブジェクトに強制的に注入する必要はありません

最初のケースは簡単に説明できます。それは言語に埋め込まれたものであるため、機能すると仮定します。それ以外の場合は、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)。

Ock笑の過負荷

Mockery (PHP単体テスト用のツールを提供するライブラリ)を使用すると、これらのスタブの記述を回避することもできます。 Mockeryでは、「インスタンスモック」(別名「オーバーロード」)を使用して、新しいインスタンスの作成をインターセプトし、モックに置き換えることができます。 この記事 それを行う方法をかなりよく説明しています。

クラスがロードされると...

テストするコードにハード依存関係があり(newを使用してクラスをインスタンス化する)、インスタンス化されるクラスをロードせずにテストをロードする可能性がない場合、コードに触れることなく(または、その周りの抽象化レイヤー)。

ただし、テストbootstrapファイルは多くの場合、絶対的な最初のものとしてロードされることに注意してください。したがって、スタブのロードを強制できる少なくとも2つのケースがあります。

  1. コードはオートローダーを使用します。この場合、オートローダーをロードする前にスタブをロードすると、newが使用されるとクラスがすでに検出され、オートローダーがトリガーされないため、「実際の」クラスはロードされません。

  2. コードは、クラスを定義/ロードする前に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() );
}
6
gmazzap