web-dev-qa-db-ja.com

PHPUnitを使用したプライベートメソッドのモック

PHPUnitを使用してクラス内のプライベートメソッドをモックすることについて質問があります。例で紹介しましょう。

class A {
  public function b() { 
    // some code
    $this->c(); 
    // some more code
  }

  private function c(){ 
    // some code
  }
}

プライベートメソッドの結果をスタブして、パブリック関数のいくつかのコード部分をテストするにはどうすればよいですか。

部分的に読み取りを解決 こちら

65
Tony

通常、プライベートおよび保護されたメソッドを直接テストしたり、モックしたりすることはありません。

テストするのは、クラスのpublicAPIです。それ以外はすべて、クラスの実装の詳細であり、テストを変更してもテストを「壊さない」ようにしてください。

また、パブリックAPIを呼び出すと実行できないコードがクラスに含まれている可能性があるため、「100%のコードカバレッジが得られない」ことに気づいたときに役立ちます。


あなたは通常これをしたくない

しかし、クラスが次のように見える場合:

class a {

    public function b() {
        return 5 + $this->c();
    }

    private function c() {
        return mt_Rand(1,3);
    }
}

「ランダム」関数はグローバルな状態であり、それをテストできないため、c())をモックアウトする必要があることがわかります。

「clean?/ verbose?/ overcomplicated-maybe?/ i-like-it-usually」ソリューション

class a {

    public function __construct(RandomGenerator $foo) {
        $this->foo = $foo;
    }

    public function b() {
        return 5 + $this->c();
    }

    private function c() {
        return $this->foo->Rand(1,3);
    }
}

グローバルが含まれておらず、うまくテストできるため、「c()」をモックアウトする必要はもうありません。


あなたがプライベート関数からグローバル状態を削除したくない、または削除できない場合(悪いこと悪い現実またはあなたの悪い定義は異なるかもしれません)あなたはcanモックに対してテストします。

// maybe set the function protected for this to work
$testMe = $this->getMock("a", array("c"));
$testMe->expects($this->once())->method("c")->will($this->returnValue(123123));

そして、このモックに対してテストを実行するのは、取り出す/モックする関数が「c()」だけだからです。


「実用的な単体テスト」の本を引用するには:

「一般的に、テストのためにカプセル化を破りたくありません(または、お母さんが「プライベートを公開しないでください!」と言っていたように)。ほとんどの場合、クラスをテストできるはずです。プライベートメソッドまたは保護されたアクセスの背後に隠されている重要な機能がある場合、それは脱出に苦労している別のクラスがあるという警告サインかもしれません。」


さらにいくつか: Why you don't want test private methods.

81
edorian

プライベートメソッドのテスト はできますが、このメソッドの実行をシミュレート(モック)することはできません。

さらに、リフレクションでは、プライベートメソッドを保護されたメソッドまたはパブリックメソッドに変換することはできません。 setAccessible は、元のメソッドの呼び出しのみを許可します。

または、プライベートメソッドの名前を変更し、「新しい実装」を含めるために runkit を使用できます。ただし、これらの機能は実験的なものであり、使用は推奨されません。

25
doctore

テストで reflection および setAccessible() を使用すると、オブジェクトの内部状態を設定して、オブジェクトが返すものを返すことができます。プライベートメソッドから欲しい。 PHP 5.3.2。

$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
24
David Harkness

Protected methodのモックを取得できるため、Cをprotectedに変換できる場合、このコードが役立ちます。

 $mock = $this->getMockBuilder('A')
                  ->disableOriginalConstructor()
                  ->setMethods(array('C'))
                  ->getMock();

    $response = $mock->B();

これは間違いなく動作します、それは私のために働いた。その後、プロテクトメソッドCをカバーするために、リフレクションクラスを使用できます。

13
Archit Rastogi

$ myClass-> privateMethodX($ arg1、$ arg2)をテストする必要があると仮定すると、リフレクションでこれを行うことができます。

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
10
Edson Medina

このような呼び出しを1行にするために使用できる他の回答のバリエーションを次に示します。

public function callPrivateMethod($object, $methodName)
{
    $reflectionClass = new \ReflectionClass($object);
    $reflectionMethod = $reflectionClass->getMethod($methodName);
    $reflectionMethod->setAccessible(true);

    $params = array_slice(func_get_args(), 2); //get all the parameters after $methodName
    return $reflectionMethod->invokeArgs($object, $params);
}
9
Mark McEver

私の場合、この汎用クラスを思いつきました。

/**
 * @author Torge Kummerow
 */
class Liberator {
    private $originalObject;
    private $class;

    public function __construct($originalObject) {
        $this->originalObject = $originalObject;
        $this->class = new ReflectionClass($originalObject);
    }

    public function __get($name) {
        $property = $this->class->getProperty($name);
        $property->setAccessible(true);
        return $property->getValue($this->originalObject);
    }

    public function __set($name, $value) {
        $property = $this->class->getProperty($name);            
        $property->setAccessible(true);
        $property->setValue($this->originalObject, $value);
    }

    public function __call($name, $args) {
        $method = $this->class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($this->originalObject, $args);
    }
}

このクラスを使用すると、任意のオブジェクトのすべてのプライベート関数/フィールドを簡単かつ透過的に解放できます。

$myObject = new Liberator(new MyObject());
/* @var $myObject MyObject */  //Usefull for code completion in some IDEs

//Writing to a private field
$myObject->somePrivateField = "testData";

//Reading a private field
echo $myObject->somePrivateField;

//calling a private function
$result = $myObject->somePrivateFunction($arg1, $arg2);

パフォーマンスが重要な場合は、Liberatorクラスで呼び出されるプロパティ/メソッドをキャッシュすることで改善できます。

6
Torge

1つのオプションは、protectedの代わりにc()privateを作成し、c()をサブクラス化してオーバーライドすることです。次に、サブクラスでテストします。別のオプションは、c()Aに注入できる別のクラスにリファクタリングすることです(これは依存性注入と呼ばれます)。そして、単体テストでc()のモック実装でテストインスタンスを注入します。

5
Asaph

別の解決策は、プライベートメソッドを保護されたメソッドに変更してからモックすることです。

$myMockObject = $this->getMockBuilder('MyMockClass')
        ->setMethods(array('__construct'))
        ->setConstructorArgs(array("someValue", 5))
        ->setMethods(array('myProtectedMethod'))
        ->getMock();

$response = $myMockObject->myPublicMethod();

ここで、myPublicMethodmyProtectedMethodを呼び出します。残念ながら、setMethodsはプライベートメソッドを見つけることができないため、プライベートメソッドでこれを行うことはできません。

2
Thellimist

PHP 7.を使用して匿名クラスを使用できます。

$mock = new class Concrete {
    private function bob():void
    {
    }
};

以前のバージョンのPHPでは、基本クラスを拡張するテストクラスを作成できます。

0
jgmjgm