web-dev-qa-db-ja.com

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

プライベートメソッドをテストしますか の議論が見つかりました。

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

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

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

どちらがベストプラクティスですか?他に何かありますか?

JUnitは保護されたメソッドを自動的にpublicに変更するように見えますが、私はそれを詳しく見ていません。 PHPは、これを reflection を介して許可しません。

268
GrGr

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(...));
  ...
}
394
uckelman

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

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

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

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

46
troelskn

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)
             );
35
robert.egginton

ckelman's answer で定義されているgetMethod()のわずかなバリエーションを提案したいと思います。

このバージョンでは、ハードコードされた値を削除し、使用方法を少し簡素化することで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()を作成して、予想される内容を明示しましたが、それはユーザー次第です。

乾杯!

20
teastburn

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

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

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

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

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

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

9
Michael Johnson

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

私は__callハックを使用して、さまざまな程度の成功を収めました。私が思いついた代替案は、訪問者パターンを使用することでした:

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

2:必要なメソッドと引数でプライムします

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

4:テストするクラスにそれを注入します

5:SUTは操作の結果を訪問者に注入します

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

5
sunwukung

実際に、__ 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を例に交換します。

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

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

5
David Harkness

「Henrik Paul」の回避策/アイデアの回避策は次のとおりです。

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

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

やってみて。

2
Anirudh Zala