Ricerca…


Accesso a variabili membro private e protette

Reflection viene spesso utilizzato come parte del test del software, ad esempio per la creazione / creazione di runtime di oggetti mock. È anche ottimo per controllare lo stato di un oggetto in qualsiasi momento. Ecco un esempio dell'utilizzo di Reflection in un unit test per verificare che un membro di classe protetto contenga il valore previsto.

Di seguito è riportata una classe di base per un'auto. Ha una variabile membro protetta che conterrà il valore che rappresenta il colore dell'auto. Poiché la variabile membro è protetta, non è possibile accedervi direttamente e utilizzare un metodo getter e setter per recuperare e impostare il suo valore rispettivamente.

class Car
{
    protected $color
    
    public function setColor($color)
    {
        $this->color = $color;
    }
    
    public function getColor($color)
    {
        return $this->color;
    }
}

Per testare questo, molti sviluppatori creeranno un oggetto Car, impostano il colore Car::setColor() usando Car::setColor() , recuperano il colore usando Car::getColor() e confrontano tale valore con il colore impostato:

/**
 * @test
 * @covers     \Car::setColor
 */
public function testSetColor()
{
    $color = 'Red';

    $car = new \Car();
    $car->setColor($color);
    $getColor = $car->getColor();
        
    $this->assertEquals($color, $reflectionColor);
}

In superficie, questo sembra ok. Dopotutto, tutto ciò che fa Car::getColor() restituisce il valore della variabile membro protetta Car::$color . Ma questo test è difettoso in due modi:

  1. Esercita Car::getColor() che non rientra nell'ambito di questo test
  2. Dipende da Car::getColor() che può avere un bug stesso che può rendere il test un falso positivo o negativo

Diamo un'occhiata al motivo per cui non dovremmo usare Car::getColor() nel nostro test unitario e dovremmo usare Reflection. Supponiamo che a uno sviluppatore venga assegnata un'attività che aggiunge "Metallico" a ogni colore della vettura. Quindi tentano di modificare Car::getColor() per anteporre "Metallic" al colore dell'auto:

class Car
{
    protected $color
    
    public function setColor($color)
    {
        $this->color = $color;
    }
    
    public function getColor($color)
    {
        return "Metallic "; $this->color;
    }
}

Vedi l'errore? Lo sviluppatore ha usato un punto e virgola anziché l'operatore di concatenazione nel tentativo di anteporre "Metallico" al colore della vettura. Di conseguenza, ogni volta che viene chiamato Car::getColor() , "Metallic" viene restituito indipendentemente da quale sia il colore effettivo dell'auto. Di conseguenza, il test dell'unità Car::setColor() fallirà anche se Car::setColor() funziona perfettamente e non è influenzato da questa modifica .

Quindi, come possiamo verificare che Car::$color contenga il valore che stiamo impostando tramite Car::setColor() ? Possiamo usare la Refelection per ispezionare direttamente la variabile membro protetta. Quindi, come possiamo farlo? Possiamo usare Refelection per rendere la variabile membro protetta accessibile al nostro codice in modo che possa recuperare il valore.

Vediamo prima il codice e poi scomporlo:

/**
 * @test
 * @covers     \Car::setColor
 */
public function testSetColor()
{
    $color = 'Red';

    $car = new \Car();
    $car->setColor($color);
    
    $reflectionOfCar = new \ReflectionObject($car);
    $protectedColor = $reflectionOfForm->getProperty('color');
    $protectedColor->setAccessible(true);
    $reflectionColor = $protectedColor->getValue($car);
    
    $this->assertEquals($color, $reflectionColor);
}

Ecco come utilizziamo Reflection per ottenere il valore di Car::$color nel codice qui sopra:

  1. Creiamo un nuovo ReflectionObject che rappresenta il nostro oggetto Car
  2. Otteniamo una ReflectionProperty per Car::$color (questo "rappresenta" la variabile Car::$color )
  3. Facciamo Car::$color accessibile
  4. Otteniamo il valore di Car::$color

Come potete vedere usando Reflection, potremmo ottenere il valore di Car::$color senza dover chiamare Car::getColor() o qualsiasi altra funzione di accesso che potrebbe causare risultati di test non validi. Ora il nostro test unitario per Car::setColor() è sicuro e preciso.

Rilevamento di funzioni di classi o oggetti

Il rilevamento delle caratteristiche delle classi può essere effettuato in parte con le funzioni property_exists e method_exists .

class MyClass {
    public $public_field;
    protected $protected_field;
    private $private_field;
    static $static_field;
    const CONSTANT = 0;
    public function public_function() {}
    protected function protected_function() {}
    private function private_function() {}
    static function static_function() {}
}

// check properties
$check = property_exists('MyClass', 'public_field');    // true
$check = property_exists('MyClass', 'protected_field'); // true
$check = property_exists('MyClass', 'private_field');   // true, as of PHP 5.3.0
$check = property_exists('MyClass', 'static_field');    // true
$check = property_exists('MyClass', 'other_field');     // false

// check methods
$check = method_exists('MyClass', 'public_function');    // true
$check = method_exists('MyClass', 'protected_function');    // true
$check = method_exists('MyClass', 'private_function');    // true
$check = method_exists('MyClass', 'static_function');    // true

// however...
$check = property_exists('MyClass', 'CONSTANT');  // false
$check = property_exists($object, 'CONSTANT');    // false

Con ReflectionClass , è possibile rilevare anche costanti:

$r = new ReflectionClass('MyClass');
$check = $r->hasProperty('public_field');  // true
$check = $r->hasMethod('public_function'); // true
$check = $r->hasConstant('CONSTANT');      // true
// also works for protected, private and/or static members.

Nota: per property_exists e method_exists , è possibile method_exists anche un oggetto della classe di interesse al posto del nome della classe. Utilizzando reflection, la classe ReflectionObject deve essere utilizzata al posto di ReflectionClass .

Test di metodi privati ​​/ protetti

A volte è utile testare metodi privati ​​e protetti così come quelli pubblici.

class Car
{
    /**
     * @param mixed $argument
     *
     * @return mixed
     */
    protected function drive($argument)
    {
        return $argument;
    }

    /**
     * @return bool
     */
    private static function stop()
    {
        return true;
    }
}

Il metodo più semplice per testare il metodo di guida è l'uso della riflessione

class DriveTest
{
    /**
     * @test
     */
    public function testDrive()
    {
        // prepare
        $argument = 1;
        $expected = $argument;
        $car = new \Car();

        $reflection = new ReflectionClass(\Car::class);
        $method = $reflection->getMethod('drive');
        $method->setAccessible(true);

        // invoke logic
        $result = $method->invokeArgs($car, [$argument]);

        // test
        $this->assertEquals($expected, $result);
    }
}

Se il metodo è statico, si passa null al posto dell'istanza della classe

class StopTest
{
    /**
     * @test
     */
    public function testStop()
    {
        // prepare
        $expected = true;

        $reflection = new ReflectionClass(\Car::class);
        $method = $reflection->getMethod('stop');
        $method->setAccessible(true);

        // invoke logic
        $result = $method->invoke(null);

        // test
        $this->assertEquals($expected, $result);
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow