wake-up-neo.com

phpunit - mockbuilder - Setze interne Eigenschaft des Scheinobjekts

Kann ein Mock-Objekt mit deaktiviertem Konstruktor und manuell gesetzten geschützten Eigenschaften erstellt werden?

Hier ist ein idiotisches Beispiel:

class A {
    protected $p;
    public function __construct(){
        $this->p = 1;
    }

    public function blah(){
        if ($this->p == 2)
            throw Exception();
    }
}

class ATest extend bla_TestCase {
    /** 
        @expectedException Exception
    */
    public function testBlahShouldThrowExceptionBy2PValue(){
        $mockA = $this->getMockBuilder('A')
            ->disableOriginalConstructor()
            ->getMock();
        $mockA->p=2; //this won't work because p is protected, how to inject the p value?
        $mockA->blah();
    }
}

Also möchte ich den p-Wert injizieren, der geschützt ist, also kann ich nicht. Soll ich Setter oder IoC definieren, oder kann ich das mit phpunit?

33
inf3rno

Sie können die Eigenschaft mithilfe von Reflection öffentlich machen und dann den gewünschten Wert festlegen:

$a = new A;
$reflection = new ReflectionClass($a);
$reflection_property = $reflection->getProperty('p');
$reflection_property->setAccessible(true);

$reflection_property->setValue($a, 2);

Auf jeden Fall müssen Sie in Ihrem Beispiel keinen p-Wert festlegen, damit die Exception ausgelöst wird. Sie verwenden einen Mock, um die Kontrolle über das Objektverhalten übernehmen zu können, ohne dabei die Interna zu berücksichtigen.

Anstatt p = 2 so einzustellen, dass eine Exception ausgelöst wird, konfigurieren Sie den Mock so, dass eine Exception ausgelöst wird, wenn die blah-Methode aufgerufen wird:

$mockA = $this->getMockBuilder('A')
        ->disableOriginalConstructor()
        ->getMock();
$mockA->expects($this->any())
         ->method('blah')
         ->will($this->throwException(new Exception));

Zum Schluss ist es seltsam, dass Sie die A-Klasse im ATest verspotten. Sie machen normalerweise die Abhängigkeiten, die das von Ihnen getestete Objekt benötigt.

Hoffe das hilft.

44
gontrollez

Ich dachte, ich würde eine praktische Hilfsmethode hinterlassen, die hier schnell kopiert und eingefügt werden kann:

/**
 * Sets a protected property on a given object via reflection
 *
 * @param $object - instance in which protected value is being modified
 * @param $property - property on instance being modified
 * @param $value - new value of the property being modified
 *
 * @return void
 */
public function setProtectedProperty($object, $property, $value)
{
    $reflection = new ReflectionClass($object);
    $reflection_property = $reflection->getProperty($property);
    $reflection_property->setAccessible(true);
    $reflection_property->setValue($object, $value);
}
17
rsahai91

Es wäre erstaunlich, wenn jede Codebase DI und IoC verwenden würde und so etwas nie machen würde: 

public function __construct(BlahClass $blah)
{
    $this->protectedProperty = new FooClass($blah);
}

Sie können zwar eine Mock-BlahClass im Konstruktor verwenden, aber der Konstruktor setzt dann eine geschützte Eigenschaft auf etwas, das Sie NICHT simulieren können.

Sie denken also wahrscheinlich: "Nun, Sie müssen den Konstruktor so umgestalten, dass er anstelle von BlahClass eine FooClass verwendet. Dann müssen Sie die FooClass nicht im Konstruktor instanziieren, und Sie können stattdessen einen Mock einsetzen!" Nun, Sie hätten recht, wenn das nicht bedeutet, dass Sie jede Verwendung der Klasse in der gesamten Codebase ändern müssen, um ihr eine FooClass statt einer BlahClass zu geben.

Nicht jede Codebase ist perfekt, und manchmal müssen Sie nur etwas erledigen. Und das bedeutet, ja, manchmal müssen Sie die Regel "Nur öffentliche Test-APIs" brechen.

0
Zachary Burnham