PHPUnitで保護されたメソッドをテストするためのベストプラクティス


287

私は上の議論見つけ、あなたがプライベートメソッドのテストドゥ参考に。

一部のクラスでは、メソッドを保護したいが、それらをテストしたいと思いました。これらのメソッドのいくつかは静的で短いものです。ほとんどのパブリックメソッドはそれらを使用するため、後でテストを安全に削除できるでしょう。しかし、TDDアプローチから始めてデバッグを回避するために、私は本当にそれらをテストしたいと思います。

私は次のことを考えました:

  • 回答でアドバイスされているメソッドオブジェクトは、これでやり過ぎのようです。
  • パブリックメソッドから開始し、上位レベルのテストによってコードカバレッジが提供されたら、それらを保護してテストを削除します。
  • 保護されたメソッドをパブリックにするテスト可能なインターフェイスを持つクラスを継承します

ベストプラクティスはどれですか。他に何かありますか?

JUnitは自動的に保護されたメソッドをパブリックに変更するようですが、私はそれを深く見ていませんでした。PHPはリフレクションを介してこれを許可しません。


2つの質問:1.クラスが公開していない機能をテストする必要があるのはなぜですか?2.テストする必要がある場合、なぜプライベートなのですか?
nad2000 2011年

2
多分彼はプライベートプロパティが正しく設定されているかどうかをテストしたいと
思い

4
これはディスカッションスタイルであり、建設的ではありません。再び:)
mlvljr

72
あなたはサイトのルールに反してそれを呼ぶことができます、しかしそれを単に「建設的でない」と呼ぶことは...それは侮辱です。
アンディV

1
@Visser、それは自分自身を侮辱しています;)
Pacerier

回答:


417

PHPUnitでPHP5(> = 5.3.2)を使用している場合、テストを実行する前にリフレクションを使用してプライベートメソッドと保護されたメソッドをパブリックに設定することでテストできます。

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}

27
セバスチャンのブログへのリンクを引用するには:「だから:保護されたプライベートな属性とメソッドのテストが可能だからといって、これが「良いこと」だとは限らない。」-念頭に置いておくと
エドリアン

10
私はそれを争います。保護されたメソッドやプライベートメソッドが機能する必要がない場合は、テストしないでください。
uckelman

10
明確にするために、これが機能するためにPHPUnitを使用する必要はありません。SimpleTestなどでも動作します。PHPUnitに依存する答えについては何もありません。
Ian Dunn、

84
保護/プライベートメンバーを直接テストしないでください。これらはクラスの内部実装に属しており、テストと組み合わせるべきではありません。これはリファクタリングを不可能にし、最終的にはテストする必要があるものをテストしなくなります。パブリックメソッドを使用して間接的にテストする必要があります。これが難しい場合は、クラスの構成に問題があることを確認し、それをより小さなクラスに分離する必要があります。クラスはテスト用のブラックボックスでなければならないことに注意してください。何かを投入すると、何かが返されます。それだけです。
gphilip

24
@gphilip私にとって、protectedメソッドはパブリックAPIの一部でもあります。これは、サードパーティのクラスがそれを拡張して、魔法なしで使用できるためです。したがってprivate、メソッドのみが直接テストされないメソッドのカテゴリに分類されると思います。protectedそしてpublic、直接テストする必要があります。
Filip Halaxa

48

あなたはすでに気づいているようですが、とにかく言い直します。保護されたメソッドをテストする必要がある場合、それは悪い兆候です。ユニットテストの目的は、クラスのインターフェースをテストすることであり、保護されたメソッドは実装の詳細です。そうは言っても、それが理にかなっている場合があります。継承を使用する場合、スーパークラスがサブクラスのインターフェースを提供していると見ることができます。したがって、ここでは、保護されたメソッドをテストする必要があります(ただし、プライベートメソッドは使用しないでください)。これに対する解決策は、テスト目的でサブクラスを作成し、これを使用してメソッドを公開することです。例えば。:

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

継承は常に合成で置き換えることができることに注意してください。コードをテストする場合、通常、このパターンを使用するコードを処理する方がはるかに簡単なので、そのオプションを検討することをお勧めします。


2
stuff()をパブリックとして直接実装して、parent :: stuff()を返すだけです。私の返事を見てください。今日はあまりにも速く読んでいるようです。
マイケルジョンソン

あなたが正しい; 保護されたメソッドをパブリックメソッドに変更することは有効です。
troelskn 2008年

したがって、コードは私の3番目のオプションを提案し、「継承は常にコンポジションに置き換えることができることに注意してください。」私の最初のオプションまたはの方向に行くrefactoring.com/catalog/replaceInheritanceWithDelegation.html
GRGR

34
それが悪い兆候であることには同意しません。TDDと単体テストの違いを見てみましょう。ユニットテストはプライベートメソッドimoをテストする必要があります。これらはユニットであり、ユニットテストのパブリックメソッドがユニットテストから利益を得るのと同じ方法で利益を得るからです。
koen

36
プロテクトメソッドクラスのインターフェイスの一部であり、単なる実装の詳細ではありません。保護されたメンバーの要点は、サブクラス(ユーザー自身の権利)がクラスの説明内でこれらの保護されたメソッドを使用できるようにすることです。それらは明らかにテストする必要があります。
BT

40

teastburnには適切なアプローチがあります。さらに簡単なのは、メソッドを直接呼び出して答えを返すことです。

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

あなたはこれをあなたのテストで単に呼び出すことができます:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

1
これは良い例です、ありがとう。メソッドは保護されているのではなくパブリックであるべきですよね?
12

いい視点ね。私は実際に、テストクラスを拡張するベースクラスでこのメソッドを使用しています。この場合、これは理にかなっています。クラスの名前はここでは間違っています。
robert.egginton 2012

Teastburn xDに基づいてまったく同じコードを作成しました
Nebulosar

23

で定義されているgetMethod()にわずかなバリエーションを提案したいと思います uckelmanの回答でます

このバージョンでは、ハードコードされた値を削除し、使用法を少し簡略化することにより、getMethod()を変更しています。以下の例のように、PHPUnitUtilクラスに追加するか、PHPUnit_Framework_TestCase-extendingクラスに追加することをお勧めします(または、おそらく、PHPUnitUtilファイルにグローバルに追加します)。

MyClassはとにかくインスタンス化されており、ReflectionClassは文字列またはオブジェクトを取ることができるので...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

エイリアス関数getProtectedMethod()も作成して、予期されるものを明示的にしましたが、それはあなた次第です。

乾杯!


リフレクションクラスAPIを使用するための+1。
Bill Ortell 2013年

10

troelsknは近いと思います。代わりにこれを行います:

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}

次に、次のようなものを実装します。

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}

次に、TestClassToTestに対してテストを実行します。

コードを解析することにより、このような拡張クラスを自動的に生成できるはずです。PHPUnitがすでにそのようなメカニズムを提供していても驚かないでしょう(私はチェックしていません)。


えっと...それは私が言っているようです、3番目のオプションを使用してください:)
マイケル・ジョンソン

2
はい、それはまさに私の3番目のオプションです。PHPUnitがそのようなメカニズムを提供していないことは確かです。
GrGr 2008年

これは機能しません。保護された関数を同じ名前のパブリック関数でオーバーライドすることはできません。
公園。

私は間違っているかもしれませんが、このアプローチがうまくいくとは思いません。PHPUnit(私がこれまで使用したことがある限り)では、テストクラスが実際のテスト機能を提供する別のクラスを拡張する必要があります。それを回避する方法がない限り、私はこの答えがどのように使用されるかを確認できるかどうかわかりません。phpunit.de/manual/current/en/...
サイファー


5

ここで帽子をリングに投げ込みます。

__callハックを使用して、成功の度合いはさまざまです。私が思いついた別の方法は、訪問者パターンを使用することでした:

1:stdClassまたはカスタムクラスを生成する(型を強制するため)

2:必要なメソッドと引数でそれを準備する

3:SUTに、Visitingクラスで指定された引数を使用してメソッドを実行するacceptVisitorメソッドがあることを確認します。

4:テストするクラスに挿入します

5:SUTは操作の結果をビジターに挿入します

6:訪問者の結果属性にテスト条件を適用する


1
興味深いソリューションの+1
jsh 2014

5

実際、一般的な方法で__call()を使用して、保護されたメソッドにアクセスできます。このクラスをテストできるようにするには

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

ExampleTest.phpにサブクラスを作成します。

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

__call()メソッドはクラスを参照しないため、テストする保護されたメソッドを含む各クラスについて上記をコピーし、クラス宣言を変更するだけでよいことに注意してください。この関数を共通の基本クラスに配置できるかもしれませんが、私は試していません。

これで、テストケース自体は、テスト対象のオブジェクトを構築する場所のみが異なり、ExampleExposedをExampleに置き換えます。

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

PHP 5.3では、リフレクションを使用してメソッドのアクセシビリティを直接変更できると思いますが、メソッドごとに個別に変更する必要があると思います。


1
__call()の実装は素晴らしいです!投票を試みましたが、この方法をテストするまで投票の設定を解除し、SOの制限時間のために投票できなくなりました。
Adam Franco

このcall_user_method_array()関数はPHP 4.1.0で非推奨になりました... call_user_func_array(array($this, $method), $args)代わりに使用してください。PHP 5.3.2以降を使用している場合は、Reflectionを使用して保護された/プライベートのメソッドと属性にアクセス
nuqqsa

@nuqqsa-ありがとう、私の回答を更新しました。それ以来、Accessibleリフレクションを使用して、テストがクラスおよびオブジェクトのプライベート/保護されたプロパティおよびメソッドにアクセスできるようにする汎用パッケージを作成しました。
David Harkness、

このコードは、PHP 5.2.7では動作しません-__callメソッドは、基本クラスが定義するメソッドに対して呼び出されません。ドキュメントには記載されていませんが、この動作はPHP 5.3で変更されたと思います(動作確認済み)。
ラッセルデイビス

@Russell- __call()呼び出し元がメソッドにアクセスできない場合にのみ呼び出されます。クラスとそのサブクラスは保護されたメソッドにアクセスできるため、それらへの呼び出しは通過しません__call()。5.2.7で機能しないコードを新しい質問に投稿できますか?私は5.2で上記を使用し、リフレクションを5.3.2でのみ使用するようにしました。
David Harkness、2011年

2

「ヘンリクポール」の回避策/アイデアのための次の回避策をお勧めします:)

クラスのプライベートメソッドの名前を知っている。たとえば、_add()、_ edit()、_ delete()などです。

したがって、単体テストの側面からテストしたい場合は、いくつかの一般的なプレフィックスまたはサフィックスを付けてプライベートメソッドを呼び出すだけです。 __ call()メソッドが呼び出されたときに(メソッド_addPhpunit()が呼び出されないように、単語(たとえば_addPhpunit)のサフィックスを存在)の所有者クラスの場合、__ call()メソッドに必要なコードを挿入して、接頭辞付き/接尾辞付きの単語(Phpunit)を削除し、そこからその推論されたプライベートメソッドを呼び出します。これは、魔法の方法のもう1つの良い使い方です。

やってみよう。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.