Sök…


Få åtkomst till privata och skyddade medlemsvariabler

Reflektion används ofta som en del av mjukvarutestning, till exempel för skapande / inställning av runtime av håliga objekt. Det är också bra för att kontrollera ett objekts tillstånd vid en viss tidpunkt. Här är ett exempel på att använda Reflektion i ett enhetstest för att verifiera att en skyddad klassmedlem innehåller det förväntade värdet.

Nedan är en mycket grundläggande klass för en bil. Den har en skyddad medlemsvariabel som kommer att innehålla det värde som representerar bilens färg. Eftersom medlemsvariabeln är skyddad kan vi inte komma åt den direkt och måste använda en getter- och setter-metod för att hämta respektive ställa in dess värde.

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

För att testa detta kommer många utvecklare att skapa ett bilobjekt, ställa in bilens färg med Car::setColor() , hämta färgen med Car::getColor() och jämföra det värdet med färgen de anger:

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

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

På ytan verkar det okej. När allt kommer Car::getColor() , allt Car::getColor() gör är att returnera värdet på den skyddade medlemsvariabeln Car::$color . Men detta test är felaktigt på två sätt:

  1. Den utövar Car::getColor() som inte omfattas av testet
  2. Det beror på Car::getColor() som kan ha ett fel i sig själv som kan göra att testet har falskt positivt eller negativt

Låt oss titta på varför vi inte ska använda Car::getColor() i vårt enhetstest och ska använda Reflektion istället. Låt oss säga att en utvecklare har tilldelats en uppgift att lägga till "Metallic" till varje bilfärg. Så de försöker modifiera Car::getColor() att bero "Metallic" till bilens färg:

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

Ser du felet? Utvecklaren använde en semikolon istället för sammankopplingsoperatören i ett försök att bero "Metallic" till bilens färg. Som ett resultat Car::getColor() "Metallic" när Car::getColor() kallas oavsett vad bilens faktiska färg är. Som ett resultat misslyckas vårt Car::setColor() enhetstest trots att Car::setColor() fungerar perfekt och inte påverkades av denna förändring .

Så hur verifierar vi Car::$color innehåller det värde vi ställer in via Car::setColor() ? Vi kan använda Refelection för att inspektera den skyddade medlemsvariabeln direkt. Så hur gör vi det ? Vi kan använda Refelection för att göra den skyddade medlemmvariabeln tillgänglig för vår kod så att den kan hämta värdet.

Låt oss se koden först och sedan dela den ned:

/**
 * @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);
}

Så här använder vi Reflektion för att få värdet på Car::$color i koden ovan:

  1. Vi skapar ett nytt reflektionsobjekt som representerar vårt bilobjekt
  2. Vi får en ReflectionProperty för Car::$color (detta "representerar" Car::$color -färgsvariabeln)
  3. Vi gör Car::$color tillgänglig
  4. Vi får värdet på Car::$color

Som du kan se med hjälp av Reflektion kan vi få värdet på Car::$color utan att behöva ringa Car::getColor() eller någon annan accessorfunktion som kan orsaka ogiltiga testresultat. Nu är vårt enhetstest för Car::setColor() säkert och korrekt.

Funktionsdetektering av klasser eller objekt

Funktionsdetektering av klasser kan delvis göras med property_exists och 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

Med en ReflectionClass kan också konstanter detekteras:

$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.

Obs: för property_exists och method_exists , också ett syfte med den klass av intresse kan tillhandahållas i stället för klassnamnet. Med reflektion bör klassen ReflectionObject användas istället för ReflectionClass .

Testa privata / skyddade metoder

Ibland är det användbart att testa privata och skyddade metoder såväl som offentliga.

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

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

Det enklaste sättet att testa körmetoden är att använda reflektion

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);
    }
}

Om metoden är statisk passerar du noll på platsen för klassinstansen

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
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow